This is an automated email from the ASF dual-hosted git repository.
tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new c2bdaba624 [ZEPPELIN-6564] Route the published paragraph through the
shared react-mount loader
c2bdaba624 is described below
commit c2bdaba624b56e27ebfd380e994b3f8678571d17
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Sun Aug 23 17:06:13 2026 +0900
[ZEPPELIN-6564] Route the published paragraph through the shared
react-mount loader
### What is this PR for?
Moves the published paragraph off its own React loader and onto the shared
`react-mount` directive, and removes the dead Module Federation configuration
that this exposed.
Today there are two loaders. `published/paragraph.component.ts` injects a
script tag into `document.head` and reads `window.reactApp` directly, while the
paragraph footer goes through the `[zeppelin-react-mount]` directive added in
ZEPPELIN-6428. The two paths differ in load-failure handling, container caching
and typing. The bespoke path has no fallback at all, so a published paragraph
whose remote fails to load renders nothing.
Split into four commits.
**1. Remove dead Module Federation configuration**
The shell `webpack.config.js` declares a `reactApp` remote but never calls
`container.init` and never bundles React. Also removes the
`GenerateRemoteEntryJson` plugin, which writes a `remoteEntry.json` nothing
reads, and the unused `<at>angular-architects/module-federation` and
`ngx-build-plus` devDependencies. As a side effect the hard-coded
`reactApp<at>http://localhost:3001/remoteEntry.js` no longer ends up in
production shell bundles.
**2. Make <at>zeppelin/sdk framework-neutral**
The SDK declares `<at>angular/common` and `<at>angular/core` as
peerDependencies but imports neither, and pins them at `^8.2.9` while the
project is on Angular 21. It does use `rxjs`, which is not declared. A
non-Angular consumer, which is what the React remote is, inherits a requirement
that does not exist and misses one that does.
**3. Add ReactFeatureService as the single flag resolver**
Flag parsing was split. The published paragraph accepted both `?react=true`
and a bare `?react`, while `notebook.component.ts` required exactly
`?reactFooter=true`. Unified on the permissive rule so the URLs documented in
ZEPPELIN-6371 keep working. As a result a bare `?reactFooter` now enables the
footer as well. Both flags are experimental opt-ins; with no flag the behaviour
is unchanged.
**4. Route the published paragraph through the shared react-mount loader**
Moves the component onto the directive and makes `./PublishedParagraph`
satisfy the mount contract. It previously returned a bare unmount function,
which the directive classified as legacy and for which it made `update` a no-op.
### What type of PR is it?
Refactoring
### Todos
None
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6564
### How should this be tested?
* A production build.
* The `zeppelin-react` vitest suite, extended with a mount-contract spec
for `PublishedParagraph` mirroring the existing `ParagraphFooter` one. It
covers the handle shape, in-place update within the same subtree, the
empty-state round trip, and `onError` on a render failure.
* The Playwright suites for the published paragraph and the paragraph
footer, including the fallback path on both.
* The Angular shell has no unit test harness (ZEPPELIN-6566,
ZEPPELIN-6567), so its side is covered by Playwright only.
### Screenshots (if appropriate)
No
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? Yes
Closes #5391 from voidmatcha/mfe-prework.
Signed-off-by: ChanHo Lee <[email protected]>
---
.../e2e/models/published-paragraph-page.ts | 10 +-
.../tests/notebook/paragraph/react-footer.spec.ts | 30 +
.../notebook/published/published-paragraph.spec.ts | 30 +-
zeppelin-web-angular/package-lock.json | 705 +--------------------
zeppelin-web-angular/package.json | 2 -
.../projects/zeppelin-react/README.md | 43 +-
.../projects/zeppelin-react/package-lock.json | 14 +-
.../projects/zeppelin-react/package.json | 1 +
.../src/pages/PublishedParagraph.spec.tsx | 122 ++++
.../src/pages/PublishedParagraph.tsx | 43 +-
.../projects/zeppelin-react/vitest.config.ts | 8 +
.../projects/zeppelin-react/webpack.config.js | 48 +-
.../projects/zeppelin-sdk/package.json | 3 +-
.../pages/workspace/notebook/notebook.component.ts | 6 +-
.../published/paragraph/paragraph.component.html | 8 +-
.../published/paragraph/paragraph.component.ts | 124 ++--
.../pages/workspace/published/published.module.ts | 3 +-
.../src/app/services/public-api.ts | 1 +
.../src/app/services/react-feature.service.ts | 70 ++
.../app/share/react-mount/react-mount-handle.ts | 12 -
.../app/share/react-mount/react-mount.directive.ts | 29 +-
.../react-mount/react-remote-loader.service.ts | 8 +-
zeppelin-web-angular/webpack.config.js | 10 -
23 files changed, 405 insertions(+), 925 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
index 13293c528e..e63350da02 100644
--- a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
+++ b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
@@ -15,15 +15,23 @@ import { navigateToNotebookWithFallback } from '../utils';
import { BasePage } from './base-page';
export class PublishedParagraphPage extends BasePage {
+ readonly confirmationModal: Locator;
+ readonly angularRenderer: Locator;
+ readonly reactWidget: Locator;
+ readonly reactWidgetOrEmptyState: Locator;
private readonly errorModalContent: Locator;
private readonly errorModalOkButton: Locator;
- readonly confirmationModal: Locator;
constructor(page: Page) {
super(page);
this.errorModalContent = this.page.locator('.ant-modal-body', { hasText:
'Paragraph Not Found' }).last();
this.errorModalOkButton = page.getByRole('button', { name: 'OK' }).last();
this.confirmationModal = page.locator('div.ant-modal-confirm').last();
+ // The result count is 0 in both modes, so dynamic-forms is the
discriminator: it renders only in Angular mode.
+ this.angularRenderer =
page.locator('zeppelin-notebook-paragraph-dynamic-forms');
+ this.reactWidget =
page.locator('[data-testid="react-published-paragraph"]');
+ // Without paragraph data the remote mounts an <Empty>, so tests that only
assert "React took over" accept either.
+ this.reactWidgetOrEmptyState =
this.reactWidget.or(page.locator('.ant-alert'));
}
async navigateToNotebook(noteId: string): Promise<void> {
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
index b82065f9bc..f14c451de7 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
@@ -55,6 +55,36 @@ test.describe('React Paragraph Footer', () => {
await
expect(page.locator('[data-testid="angular-paragraph-footer"]')).toHaveCount(0);
});
+ test('with a bare reactFooter flag (no value), React footer renders', async
({ page }) => {
+ const { noteId } = testNotebook;
+
+ await test.step('When I open the notebook with a valueless reactFooter
flag', async () => {
+ await page.goto(`/#/notebook/${noteId}?reactFooter`);
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then the React footer renders and the Angular one does
not', async () => {
+ await
expect(page.locator('[data-testid="react-paragraph-footer-content"]').first()).toBeAttached({
+ timeout: 15000
+ });
+ await
expect(page.locator('[data-testid="angular-paragraph-footer"]')).toHaveCount(0);
+ });
+ });
+
+ test('with an explicit reactFooter=false, Angular footer renders', async ({
page }) => {
+ const { noteId } = testNotebook;
+
+ await test.step('When I navigate with an explicit reactFooter=false',
async () => {
+ await page.goto(`/#/notebook/${noteId}?reactFooter=false`);
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then the flag disables React and Angular renders', async
() => {
+ await
expect(page.locator('[data-testid="angular-paragraph-footer"]').first()).toBeAttached({
timeout: 15000 });
+ await
expect(page.locator('[data-testid="react-paragraph-footer"]')).toHaveCount(0);
+ });
+ });
+
test('reactFooter=true preserves the paragraph query param', async ({ page
}) => {
const { noteId, paragraphId } = testNotebook;
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
index 099212243d..fc9dce1f6f 100644
---
a/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
@@ -156,15 +156,33 @@ test.describe('Published Paragraph', () => {
await waitForZeppelinReady(page);
});
- await test.step('Then Angular result component should not be rendered',
async () => {
- await
expect(page.locator('zeppelin-notebook-paragraph-result')).toHaveCount(0, {
timeout: 10000 });
+ await test.step('Then the Angular renderer should not be used', async ()
=> {
+ await expect(publishedParagraphPage.angularRenderer).toHaveCount(0, {
timeout: 10000 });
});
await test.step('And React widget should be mounted in the container',
async () => {
- // React mount() renders <div data-testid="react-published-paragraph">
or <Empty> (Alert)
- const reactContent =
page.locator('[data-testid="react-published-paragraph"], .ant-alert');
- // JUSTIFIED: compound selector covers React success + error fallback
(.ant-alert); either may render
- await expect(reactContent).toBeAttached({ timeout: 15000 });
+ await
expect(publishedParagraphPage.reactWidgetOrEmptyState).toBeAttached({ timeout:
15000 });
+ });
+ });
+
+ test('when the remote fails to load, the published paragraph falls back to
Angular', async ({ page }) => {
+ const { noteId, paragraphId } = testNotebook;
+
+ // Dead remote: every remoteEntry.js request fails, so the mount
directive's onError fires.
+ await page.route('**/remoteEntry.js', route => route.abort());
+
+ await test.step('When the remote entry is requested and fails', async ()
=> {
+ // Angular is the default renderer, so the assertions below pass even
if React was never enabled.
+ // Awaiting the request is what proves this is a real fallback.
+ const remoteRequested = page.waitForRequest('**/remoteEntry.js');
+ await
page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}?react=true`);
+ await remoteRequested;
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then the Angular renderer takes over and React never
mounts', async () => {
+ await expect(publishedParagraphPage.angularRenderer).toHaveCount(1, {
timeout: 15000 });
+ await expect(publishedParagraphPage.reactWidget).toHaveCount(0);
});
});
});
diff --git a/zeppelin-web-angular/package-lock.json
b/zeppelin-web-angular/package-lock.json
index 661678df45..a92ac782c1 100644
--- a/zeppelin-web-angular/package-lock.json
+++ b/zeppelin-web-angular/package-lock.json
@@ -43,7 +43,6 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
- "@angular-architects/module-federation": "^21.2.2",
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-devkit/build-angular": "^21.2.13",
"@angular-eslint/builder": "21.4.0",
@@ -78,7 +77,6 @@
"lint-staged": "^15.5.2",
"monaco-editor-webpack-plugin": "7.0.1",
"ng-packagr": "^21.2.3",
- "ngx-build-plus": "^20.0.0",
"prettier": "^3.6.2",
"scandirectory": "8.1.1",
"style-loader": "^4.0.0",
@@ -314,36 +312,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@angular-architects/module-federation": {
- "version": "21.2.2",
- "resolved":
"https://registry.npmjs.org/@angular-architects/module-federation/-/module-federation-21.2.2.tgz",
- "integrity":
"sha512-aM6Oys+RGlUZ+GuVz1gx9LlJNMnb+niEtEsCQ3NmgoIMOZDBlfYnm+jbDO0F1TJWrTOBbAvfbKvJbLHfG25r8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-architects/module-federation-runtime": "~21.2.2",
- "callsite": "^1.0.0",
- "node-fetch": "^3.3.2",
- "semver": "~7.7.1",
- "word-wrap": "^1.2.5"
- }
- },
- "node_modules/@angular-architects/module-federation-runtime": {
- "version": "21.2.2",
- "resolved":
"https://registry.npmjs.org/@angular-architects/module-federation-runtime/-/module-federation-runtime-21.2.2.tgz",
- "integrity":
"sha512-Akl6fLcD2dYXqxW4pLVgytq7NKQ998g4OUjEgcLQnhqyvlu+qdPRUfSDPlTGUiIY01/J2M9ZXcIWmUJJ5tOT5Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "peerDependencies": {
- "@angular/common": "^21.2.0",
- "@angular/core": "^21.2.0",
- "@module-federation/enhanced": "^2.2.2",
- "@module-federation/runtime-core": "^2.2.2"
- }
- },
"node_modules/@angular-builders/common": {
"version": "5.0.3",
"resolved":
"https://registry.npmjs.org/@angular-builders/common/-/common-5.0.3.tgz",
@@ -5995,278 +5963,6 @@
}
}
},
- "node_modules/@module-federation/bridge-react-webpack-plugin": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.5.0.tgz",
- "integrity":
"sha512-Ux9XVW//K6K+KHKPdc0Jnc7RtTpZaEXgbVhp5yovtFkCJVt8hEClcTeuI18MvvLiV/q2hUpCU5Wsf9zNaIYStQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/sdk": "2.5.0",
- "@types/semver": "7.5.8",
- "semver": "7.6.3"
- }
- },
-
"node_modules/@module-federation/bridge-react-webpack-plugin/node_modules/semver":
{
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity":
"sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
- "dev": true,
- "license": "ISC",
- "peer": true,
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@module-federation/cli": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/cli/-/cli-2.5.0.tgz",
- "integrity":
"sha512-+czXA6yoiiF9W6+YEOCpQE6zpGZpA89X0oCEz3EaWPTkL4chEbxurjpME8CMnJk9iuFxl167+cBQiQlVBiHGGg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/dts-plugin": "2.5.0",
- "@module-federation/sdk": "2.5.0",
- "commander": "11.1.0",
- "jiti": "2.4.2"
- },
- "bin": {
- "mf": "bin/mf.js"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@module-federation/dts-plugin": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.5.0.tgz",
- "integrity":
"sha512-q7KDhJ5tn2HrUV7uMuh/L3TaaztUosE+4LAb90sxx0pPPqWRwlpBpxu1REubv5BWXmU1K/Ozn14u6jRbjLVaGA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/error-codes": "2.5.0",
- "@module-federation/managers": "2.5.0",
- "@module-federation/sdk": "2.5.0",
- "@module-federation/third-party-dts-extractor": "2.5.0",
- "adm-zip": "0.5.10",
- "ansi-colors": "4.1.3",
- "isomorphic-ws": "5.0.0",
- "node-schedule": "2.1.1",
- "undici": "7.24.7",
- "ws": "8.18.0"
- },
- "peerDependencies": {
- "typescript": "^4.9.0 || ^5.0.0",
- "vue-tsc": ">=1.0.24"
- },
- "peerDependenciesMeta": {
- "vue-tsc": {
- "optional": true
- }
- }
- },
- "node_modules/@module-federation/enhanced": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.5.0.tgz",
- "integrity":
"sha512-P91tzwyKSCQ6AwirqvAvTqWqmTY79ndpH0uenejFw+bbLpWrjuY0q+iZUXCV/7CSNmqwH2bkA/ssuyZljmcMVQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/bridge-react-webpack-plugin": "2.5.0",
- "@module-federation/cli": "2.5.0",
- "@module-federation/dts-plugin": "2.5.0",
- "@module-federation/error-codes": "2.5.0",
- "@module-federation/inject-external-runtime-core-plugin": "2.5.0",
- "@module-federation/managers": "2.5.0",
- "@module-federation/manifest": "2.5.0",
- "@module-federation/rspack": "2.5.0",
- "@module-federation/runtime-tools": "2.5.0",
- "@module-federation/sdk": "2.5.0",
- "@module-federation/webpack-bundler-runtime": "2.5.0",
- "schema-utils": "4.3.0",
- "tapable": "2.3.0",
- "upath": "2.0.1"
- },
- "bin": {
- "mf": "bin/mf.js"
- },
- "peerDependencies": {
- "typescript": "^4.9.0 || ^5.0.0",
- "vue-tsc": ">=1.0.24",
- "webpack": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- },
- "vue-tsc": {
- "optional": true
- },
- "webpack": {
- "optional": true
- }
- }
- },
- "node_modules/@module-federation/error-codes": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.5.0.tgz",
- "integrity":
"sha512-sq05/8Gp3csy1nr2/f76K3vLy0/xRqVtP71ibGy8BiLg7h1UxWN7G4EwAKSrPZ4FnsERGeFlIszg5Z+MqlwhFg==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
- "node_modules/@module-federation/inject-external-runtime-core-plugin": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.5.0.tgz",
- "integrity":
"sha512-e2KyTHpesBrPXGHMh4d4+s2xBiNoxbiFJkPRYHMCl81a/Gu+byrMkriZcV4VM/TFvBIlrgOJisVc1nnBI5UDRQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "peerDependencies": {
- "@module-federation/runtime-tools": "2.5.0"
- }
- },
- "node_modules/@module-federation/managers": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/managers/-/managers-2.5.0.tgz",
- "integrity":
"sha512-9b5mU/7OYbKrYUJmhZ1kkfeJCZqR7qX6/FWp+oOfZMzUynN7Rb41dwoUs3TdnOKzbZ3CCwtZ2WsR4pF9ZNvuJA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/sdk": "2.5.0",
- "find-pkg": "2.0.0"
- }
- },
- "node_modules/@module-federation/manifest": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.5.0.tgz",
- "integrity":
"sha512-pmwQCGWjM2oKY7CkR7nEDOfMK0bNFJUifuDxuOB5iOWhU+Rp92UyyBI9IbJAtiISTSFGtuKRy40peJGvQq2VcQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/dts-plugin": "2.5.0",
- "@module-federation/managers": "2.5.0",
- "@module-federation/sdk": "2.5.0",
- "find-pkg": "2.0.0"
- }
- },
- "node_modules/@module-federation/rspack": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.5.0.tgz",
- "integrity":
"sha512-OAFMpMXuLEQFmWBuC1I7LNDQ8N3CDANXe0YGPWkIPNxKq5Tj/KNfDidmutoYgvXlZKOM4yKBKBsL6Xt/UvtOIw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/bridge-react-webpack-plugin": "2.5.0",
- "@module-federation/dts-plugin": "2.5.0",
- "@module-federation/inject-external-runtime-core-plugin": "2.5.0",
- "@module-federation/managers": "2.5.0",
- "@module-federation/manifest": "2.5.0",
- "@module-federation/runtime-tools": "2.5.0",
- "@module-federation/sdk": "2.5.0"
- },
- "peerDependencies": {
- "@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0",
- "typescript": "^4.9.0 || ^5.0.0",
- "vue-tsc": ">=1.0.24"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- },
- "vue-tsc": {
- "optional": true
- }
- }
- },
- "node_modules/@module-federation/runtime": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.5.0.tgz",
- "integrity":
"sha512-dOc7pFEf8aruHBk5hoJLnvwkCa5ELT78q3o9dqcdaa/TT74X5z0FT0BsaGaRBPcse/iP6czK3fWd7RLv5ZKP5g==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/error-codes": "2.5.0",
- "@module-federation/runtime-core": "2.5.0",
- "@module-federation/sdk": "2.5.0"
- }
- },
- "node_modules/@module-federation/runtime-core": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.5.0.tgz",
- "integrity":
"sha512-STmhQ3c6/hunba2FMP6GrHazXU/8GuN7Gk4dOkWNRpnqYIoD8Wx4MNl76j3HdCzBESC7uSMXTniksVaM1+xxyA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/error-codes": "2.5.0",
- "@module-federation/sdk": "2.5.0"
- }
- },
- "node_modules/@module-federation/runtime-tools": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.5.0.tgz",
- "integrity":
"sha512-fR3Na6V78ov3/O17Mev+1vydfmqlYWP4ZNxD/bBkmqKhCO7jMdthNTT02yDljlCyhYl6+X90UJlFhwFle6rIsw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/runtime": "2.5.0",
- "@module-federation/webpack-bundler-runtime": "2.5.0"
- }
- },
- "node_modules/@module-federation/sdk": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.5.0.tgz",
- "integrity":
"sha512-ScU22XDyV77l50njjzewMpMlNN1CYo0tHS1D6iy+vNKWrHGq8DWVB0vwG8dmvx/WZ4uq+sXgUsQet17MoKsfZw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "peerDependencies": {
- "node-fetch": "^2.7.0 || ^3.3.2"
- },
- "peerDependenciesMeta": {
- "node-fetch": {
- "optional": true
- }
- }
- },
- "node_modules/@module-federation/third-party-dts-extractor": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.5.0.tgz",
- "integrity":
"sha512-5di43LGk2ies86Cj8QyzYr540Ijc+nyPqYziyFotL6Pparnu+uf3b3ERfEyQfBmEcyGk1MpitQIO2J3bd9BcNw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "find-pkg": "2.0.0",
- "resolve": "1.22.8"
- }
- },
- "node_modules/@module-federation/webpack-bundler-runtime": {
- "version": "2.5.0",
- "resolved":
"https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.5.0.tgz",
- "integrity":
"sha512-UxVad+tNZYkBnZzqJQsZa0pB5gO5cJoCjMumOo3bhzXBJVqHsFupfeHa8Nk7WrRVbJE6zRT9ZHK0s0NDWBMyJw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@module-federation/error-codes": "2.5.0",
- "@module-federation/runtime": "2.5.0",
- "@module-federation/sdk": "2.5.0"
- }
- },
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.4",
"resolved":
"https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz",
@@ -8163,6 +7859,7 @@
"integrity":
"sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.7.11",
@@ -8333,6 +8030,7 @@
"integrity":
"sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"dependencies": {
"@module-federation/runtime-tools": "0.22.0",
@@ -8357,6 +8055,7 @@
"integrity":
"sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true
},
"node_modules/@rspack/core/node_modules/@module-federation/runtime": {
@@ -8365,6 +8064,7 @@
"integrity":
"sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"dependencies": {
"@module-federation/error-codes": "0.22.0",
@@ -8378,6 +8078,7 @@
"integrity":
"sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"dependencies": {
"@module-federation/error-codes": "0.22.0",
@@ -8390,6 +8091,7 @@
"integrity":
"sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"dependencies": {
"@module-federation/runtime": "0.22.0",
@@ -8402,6 +8104,7 @@
"integrity":
"sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true
},
"node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime":
{
@@ -8410,6 +8113,7 @@
"integrity":
"sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"dependencies": {
"@module-federation/runtime": "0.22.0",
@@ -8422,6 +8126,7 @@
"integrity":
"sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true
},
"node_modules/@rtsao/scc": {
@@ -8840,14 +8545,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/semver": {
- "version": "7.5.8",
- "resolved":
"https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity":
"sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
@@ -9594,17 +9291,6 @@
"node": ">=0.8"
}
},
- "node_modules/adm-zip": {
- "version": "0.5.10",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
- "integrity":
"sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.0"
- }
- },
"node_modules/agent-base": {
"version": "4.3.0",
"resolved":
"https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
@@ -10524,15 +10210,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/callsite": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
- "integrity":
"sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==",
- "dev": true,
- "engines": {
- "node": "*"
- }
- },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -10864,17 +10541,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/commander": {
- "version": "11.1.0",
- "resolved":
"https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
- "integrity":
"sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=16"
- }
- },
"node_modules/comment-parser": {
"version": "1.4.1",
"resolved":
"https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
@@ -11289,20 +10955,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/cron-parser": {
- "version": "4.9.0",
- "resolved":
"https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
- "integrity":
"sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "luxon": "^3.2.1"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/cross-env": {
"version": "10.1.0",
"resolved":
"https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
@@ -11636,16 +11288,6 @@
"lodash": "^4.17.15"
}
},
- "node_modules/data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved":
"https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity":
"sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@@ -13298,20 +12940,6 @@
"node": ">=0.8"
}
},
- "node_modules/expand-tilde": {
- "version": "2.0.2",
- "resolved":
"https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
- "integrity":
"sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "homedir-polyfill": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/expect-type": {
"version": "1.4.0",
"resolved":
"https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -13467,30 +13095,6 @@
"integrity":
"sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==",
"license": "MIT"
},
- "node_modules/fetch-blob": {
- "version": "3.2.0",
- "resolved":
"https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity":
"sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- },
- "engines": {
- "node": "^12.20 || >= 14.13"
- }
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved":
"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -13556,34 +13160,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/find-file-up": {
- "version": "2.0.1",
- "resolved":
"https://registry.npmjs.org/find-file-up/-/find-file-up-2.0.1.tgz",
- "integrity":
"sha512-qVdaUhYO39zmh28/JLQM5CoYN9byEOKEH4qfa8K1eNV17W0UUMJ9WgbR/hHFH+t5rcl+6RTb5UC7ck/I+uRkpQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "resolve-dir": "^1.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-pkg": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-2.0.0.tgz",
- "integrity":
"sha512-WgZ+nKbELDa6N3i/9nrHeNznm+lY3z4YfhDDWgW+5P0pdmMj26bxaxU11ookgY3NyP9GC7HvZ9etp0jRFqGEeQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "find-file-up": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -13792,19 +13368,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/formdata-polyfill": {
- "version": "4.0.10",
- "resolved":
"https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity":
"sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fetch-blob": "^3.1.2"
- },
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -14093,62 +13656,6 @@
"dev": true,
"license": "BSD-2-Clause"
},
- "node_modules/global-modules": {
- "version": "1.0.0",
- "resolved":
"https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
- "integrity":
"sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "global-prefix": "^1.0.1",
- "is-windows": "^1.0.1",
- "resolve-dir": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix": {
- "version": "1.0.2",
- "resolved":
"https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
- "integrity":
"sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "expand-tilde": "^2.0.2",
- "homedir-polyfill": "^1.0.1",
- "ini": "^1.3.4",
- "is-windows": "^1.0.1",
- "which": "^1.2.14"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix/node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity":
"sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true,
- "license": "ISC",
- "peer": true
- },
- "node_modules/global-prefix/node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity":
"sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
- "license": "ISC",
- "peer": true,
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -14342,20 +13849,6 @@
"node": "*"
}
},
- "node_modules/homedir-polyfill": {
- "version": "1.0.3",
- "resolved":
"https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
- "integrity":
"sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "parse-passwd": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/hono": {
"version": "4.12.23",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
@@ -15449,17 +14942,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-windows": {
- "version": "1.0.2",
- "resolved":
"https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
- "integrity":
"sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
@@ -15499,17 +14981,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/isomorphic-ws": {
- "version": "5.0.0",
- "resolved":
"https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
- "integrity":
"sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "peerDependencies": {
- "ws": "*"
- }
- },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved":
"https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -15558,6 +15029,7 @@
"integrity":
"sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"license": "MIT",
+ "optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -16622,14 +16094,6 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
- "node_modules/long-timeout": {
- "version": "0.1.1",
- "resolved":
"https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
- "integrity":
"sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
@@ -16649,17 +16113,6 @@
"yallist": "^3.0.2"
}
},
- "node_modules/luxon": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
- "integrity":
"sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved":
"https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -17446,21 +16899,6 @@
"url": "https://opencollective.com/date-fns"
}
},
- "node_modules/ngx-build-plus": {
- "version": "20.0.0",
- "resolved":
"https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-20.0.0.tgz",
- "integrity":
"sha512-cm1ZMTACAN3DEqBt/alS84zwVGgL5HAl5Dk/wh7CPyGUBQnLaxiAhjFZ6iykxgSO3e9ebIZmDBvTC480piC1eA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "webpack-merge": "^6.0.0"
- },
- "peerDependencies": {
- "@angular-devkit/build-angular": ">=20.0.0",
- "@schematics/angular": ">=20.0.0",
- "rxjs": ">= 6.0.0"
- }
- },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved":
"https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -17469,27 +16907,6 @@
"license": "MIT",
"optional": true
},
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved":
"https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity":
"sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "deprecated": "Use your platform's native DOMException instead",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=10.5.0"
- }
- },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved":
"https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -17519,25 +16936,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved":
"https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity":
"sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
- }
- },
"node_modules/node-gyp": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz",
@@ -17625,22 +17023,6 @@
"node": ">=18"
}
},
- "node_modules/node-schedule": {
- "version": "2.1.1",
- "resolved":
"https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
- "integrity":
"sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "cron-parser": "^4.2.0",
- "long-timeout": "0.1.1",
- "sorted-array-functions": "^1.3.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/nopt": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
@@ -18289,17 +17671,6 @@
"node": ">= 0.10"
}
},
- "node_modules/parse-passwd": {
- "version": "1.0.0",
- "resolved":
"https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity":
"sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/parse-statements": {
"version": "1.0.11",
"resolved":
"https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz",
@@ -19241,21 +18612,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/resolve-dir": {
- "version": "1.0.1",
- "resolved":
"https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
- "integrity":
"sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "expand-tilde": "^2.0.0",
- "global-modules": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved":
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -20326,14 +19682,6 @@
"node": ">= 14"
}
},
- "node_modules/sorted-array-functions": {
- "version": "1.3.0",
- "resolved":
"https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
- "integrity":
"sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/source-map": {
"version": "0.7.6",
"resolved":
"https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@@ -21496,17 +20844,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/undici": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
- "integrity":
"sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=20.18.1"
- }
- },
"node_modules/undici-types": {
"version": "6.20.0",
"resolved":
"https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -21568,18 +20905,6 @@
"node": ">= 0.8"
}
},
- "node_modules/upath": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
- "integrity":
"sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=4",
- "yarn": "*"
- }
- },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved":
"https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -22226,16 +21551,6 @@
"license": "MIT",
"optional": true
},
- "node_modules/web-streams-polyfill": {
- "version": "3.3.3",
- "resolved":
"https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
- "integrity":
"sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved":
"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
diff --git a/zeppelin-web-angular/package.json
b/zeppelin-web-angular/package.json
index dbf8a7ab08..2324d962bc 100644
--- a/zeppelin-web-angular/package.json
+++ b/zeppelin-web-angular/package.json
@@ -72,7 +72,6 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
- "@angular-architects/module-federation": "^21.2.2",
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-devkit/build-angular": "^21.2.13",
"@angular-eslint/builder": "21.4.0",
@@ -107,7 +106,6 @@
"lint-staged": "^15.5.2",
"monaco-editor-webpack-plugin": "7.0.1",
"ng-packagr": "^21.2.3",
- "ngx-build-plus": "^20.0.0",
"prettier": "^3.6.2",
"scandirectory": "8.1.1",
"style-loader": "^4.0.0",
diff --git a/zeppelin-web-angular/projects/zeppelin-react/README.md
b/zeppelin-web-angular/projects/zeppelin-react/README.md
index f9d1b1d631..293fe0541f 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/README.md
+++ b/zeppelin-web-angular/projects/zeppelin-react/README.md
@@ -46,22 +46,37 @@ The published paragraph was picked as pilot because it's
read-only and has almos
## Architecture
```
-Angular host (port 4200) React remote (port 3001)
-┌─────────────────────────┐ ┌─────────────────────────┐
-│ paragraph.component.ts │ │ webpack.config.js │
-│ loads remoteEntry.js ──┼──────>│ ModuleFederationPlugin │
-│ calls mount(el, props) │ │ name: 'reactApp' │
-└─────────────────────────┘ │ exposes: │
- │ ./PublishedParagraph │
- └─────────────────────────┘
+Angular host (port 4200) React remote (port 3001)
+┌───────────────────────────────┐ ┌─────────────────────────┐
+│ [zeppelin-react-mount] │ │ webpack.config.js │
+│ ReactRemoteLoaderService ────┼────>│ ModuleFederationPlugin │
+│ loads remoteEntry.js │ │ name: 'reactApp' │
+│ calls mount(el, props) │ │ exposes: │
+└───────────────────────────────┘ │ ./PublishedParagraph │
+ │ ./ParagraphFooter │
+ └─────────────────────────┘
```
-1. Angular loads `remoteEntry.js` from the React dev server or production
assets.
+1. `ReactRemoteLoaderService` loads `remoteEntry.js` from the React dev server
or production assets, once per page.
2. The script registers `window.reactApp` as a Module Federation container.
-3. Angular calls `container.get('./PublishedParagraph')` to get the module.
-4. The module exports `mount(element, props)`, which calls `createRoot()` and
renders into the DOM element.
+3. The service calls `container.get('<exposed key>')` and caches the module
promise.
+4. The `[zeppelin-react-mount]` directive calls `mount(element, props)`
outside the Angular zone and keeps the returned handle for later `update()` and
`unmount()` calls.
+5. If the remote fails to load, the directive reports the error to the host,
which renders its Angular fallback instead.
-Append `?react=true` to any published paragraph URL to activate React mode.
+Host components do not touch `window.reactApp` themselves; they bind props to
the directive and supply an `onError` callback.
+
+## Feature flags
+
+Each React surface is behind a URL query flag, resolved by
`ReactFeatureService`:
+
+| URL | Result |
+| --- | --- |
+| `?react=true` | enabled |
+| `?react` | enabled |
+| `?react=false` | disabled |
+| flag absent | disabled |
+
+Append `?react=true` to any published paragraph URL, or `?reactFooter=true` to
a notebook URL, to activate React mode.
## Setup
@@ -146,7 +161,5 @@ export function mount(element: HTMLElement, props: Props):
ReactMountHandle;
`exampleFeatureProps` should be a getter on the host component (not
an inline object literal) so identity is stable when nothing changed.
-The legacy `./PublishedParagraph` module returns a bare unmount fn from
-`mount`. The directive tolerates that shape, but new modules should use
-the handle contract.
+Every exposed module must return the handle contract from `mount`. The
directive assigns the return value straight to its handle, so returning a bare
unmount function makes the next prop change throw.
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index 53b1f40349..757bd13946 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -17,6 +17,7 @@
"file-saver": "2.0.5",
"react": "18.3.1",
"react-dom": "18.3.1",
+ "rxjs": "^7.8.0",
"xlsx-js-style": "1.2.0"
},
"devDependencies": {
@@ -53,8 +54,7 @@
"tslib": "^2.0.0"
},
"peerDependencies": {
- "@angular/common": "^8.2.9",
- "@angular/core": "^8.2.9"
+ "rxjs": "^7.0.0"
}
},
"node_modules/@ant-design/colors": {
@@ -8832,6 +8832,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity":
"sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/safe-array-concat": {
"version": "1.1.4",
"resolved":
"https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
@@ -9954,7 +9963,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity":
"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD"
},
"node_modules/tsyringe": {
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package.json
b/zeppelin-web-angular/projects/zeppelin-react/package.json
index 3cca715db9..3dd2c0e0ce 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package.json
@@ -23,6 +23,7 @@
"file-saver": "2.0.5",
"react": "18.3.1",
"react-dom": "18.3.1",
+ "rxjs": "^7.8.0",
"xlsx-js-style": "1.2.0"
},
"devDependencies": {
diff --git
a/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.spec.tsx
b/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.spec.tsx
new file mode 100644
index 0000000000..0999b7a71e
--- /dev/null
+++
b/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.spec.tsx
@@ -0,0 +1,122 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { act } from 'react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { DatasetType } from '@zeppelin/sdk';
+import { mount, PublishedParagraphMountHandle, PublishedParagraphProps } from
'./PublishedParagraph';
+
+const textResult = (data: string) => ({ type: DatasetType.TEXT, data });
+
+const baseProps: PublishedParagraphProps = {
+ paragraphId: 'paragraph-1',
+ results: [textResult('first output')]
+};
+
+describe('PublishedParagraph mount contract', () => {
+ let host: HTMLElement | null = null;
+ let handle: PublishedParagraphMountHandle | null = null;
+
+ const mountParagraph = (props?: PublishedParagraphProps): void => {
+ host = document.createElement('div');
+ document.body.appendChild(host);
+ act(() => {
+ handle = mount(host as HTMLElement, props);
+ });
+ };
+
+ afterEach(() => {
+ if (handle) {
+ const h = handle;
+ act(() => h.unmount());
+ handle = null;
+ }
+ host?.remove();
+ host = null;
+ vi.restoreAllMocks();
+ });
+
+ it('throws when no element is given', () => {
+ expect(() => mount(null as unknown as HTMLElement,
baseProps)).toThrow('Mount element is required');
+ });
+
+ it('returns an update/unmount handle and renders the results', () => {
+ mountParagraph(baseProps);
+
+ expect(typeof handle!.update).toBe('function');
+ expect(typeof handle!.unmount).toBe('function');
+
+ const rendered =
host!.querySelector('[data-testid="react-published-paragraph"]');
+ expect(rendered).not.toBeNull();
+ expect(rendered!.textContent).toContain('first output');
+ });
+
+ it('renders the empty state when the paragraph has no results', () => {
+ mountParagraph({ ...baseProps, results: [] });
+
+
expect(host!.querySelector('[data-testid="react-published-paragraph"]')).toBeNull();
+ expect(host!.textContent).toContain('No paragraph data found');
+ });
+
+ it('renders the empty state when mounted without props at all', () => {
+ mountParagraph();
+
+ expect(host!.textContent).toContain('No paragraph data found');
+ });
+
+ it('update() re-renders in place with new results', () => {
+ mountParagraph(baseProps);
+ const rootBefore = host!.firstElementChild;
+
+ const h = handle!;
+ act(() => h.update({ ...baseProps, results: [textResult('second output')]
}));
+
+ const rendered =
host!.querySelector('[data-testid="react-published-paragraph"]')!;
+ expect(rendered.textContent).toContain('second output');
+ expect(rendered.textContent).not.toContain('first output');
+ // Same host subtree, not a remount. In-place update, which is what the
host relies on.
+ expect(host!.firstElementChild).toBe(rootBefore);
+ });
+
+ it('update() can move the paragraph back to the empty state', () => {
+ mountParagraph(baseProps);
+
+ const h = handle!;
+ act(() => h.update({ ...baseProps, results: [] }));
+
+
expect(host!.querySelector('[data-testid="react-published-paragraph"]')).toBeNull();
+ expect(host!.textContent).toContain('No paragraph data found');
+ });
+
+ it('unmount() empties the host element', () => {
+ mountParagraph(baseProps);
+ const h = handle!;
+ handle = null;
+
+ act(() => h.unmount());
+
+ expect(host!.innerHTML).toBe('');
+ });
+
+ it('reports render failures through onError instead of throwing at the
host', () => {
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
+ const onError = vi.fn();
+
+ // Truthy with a length but not an array, so results.map() throws during
render.
+ const malformed = { length: 1 } as unknown as
PublishedParagraphProps['results'];
+
+ expect(() => mountParagraph({ ...baseProps, results: malformed, onError
})).not.toThrow();
+
+ expect(host!.innerHTML).toBe('');
+ expect(onError).toHaveBeenCalledTimes(1);
+ });
+});
diff --git
a/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx
b/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx
index 04ea1024b7..df803619ee 100644
---
a/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx
+++
b/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx
@@ -10,8 +10,8 @@
* limitations under the License.
*/
-import { createRoot } from 'react-dom/client';
-import { Empty } from '@/components';
+import { createRoot, Root } from 'react-dom/client';
+import { Empty, ReactErrorBoundary } from '@/components';
import { SingleResultRenderer } from '@/templates';
import { ZeppelinThemeProvider } from '@/theme';
import type { ParagraphConfigResults, ParagraphIResultsMsgItem } from
'@zeppelin/sdk';
@@ -22,6 +22,7 @@ export interface PublishedParagraphProps {
paragraphId: string;
results?: ParagraphIResultsMsgItem[];
config?: ParagraphConfigResults;
+ onError?: (error: unknown) => void;
}
export const PublishedParagraph = ({ results, config }:
PublishedParagraphProps) => (
@@ -42,22 +43,38 @@ export const PublishedParagraph = ({ results, config }:
PublishedParagraphProps)
</ZeppelinThemeProvider>
);
-export const mount = (element: HTMLElement, props?: PublishedParagraphProps)
=> {
+export interface PublishedParagraphMountHandle {
+ update: (props: PublishedParagraphProps) => void;
+ unmount: () => void;
+}
+
+export const mount = (element: HTMLElement, initialProps?:
PublishedParagraphProps): PublishedParagraphMountHandle => {
if (!element) {
throw new Error('Mount element is required');
}
- const root = createRoot(element);
+ const root: Root = createRoot(element);
+
+ const renderWith = (props?: PublishedParagraphProps) => {
+ root.render(
+ <ReactErrorBoundary onError={props?.onError}>
+ <PublishedParagraph
+ paragraphId={props?.paragraphId || 'demo-paragraph'}
+ results={props?.results}
+ config={props?.config}
+ />
+ </ReactErrorBoundary>
+ );
+ };
- root.render(
- <PublishedParagraph
- paragraphId={props?.paragraphId || 'demo-paragraph'}
- results={props?.results}
- config={props?.config}
- />
- );
+ renderWith(initialProps);
- return () => {
- root.unmount();
+ return {
+ update: (newProps: PublishedParagraphProps) => {
+ renderWith(newProps);
+ },
+ unmount: () => {
+ root.unmount();
+ }
};
};
diff --git a/zeppelin-web-angular/projects/zeppelin-react/vitest.config.ts
b/zeppelin-web-angular/projects/zeppelin-react/vitest.config.ts
index 8fde66b75c..ffddd37ae3 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/vitest.config.ts
+++ b/zeppelin-web-angular/projects/zeppelin-react/vitest.config.ts
@@ -10,9 +10,17 @@
* limitations under the License.
*/
+import path from 'path';
import { defineConfig } from 'vitest/config';
export default defineConfig({
+ // Kept in sync with the `resolve.alias` block in webpack.config.js.
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ '@zeppelin/sdk': path.resolve(__dirname, '../zeppelin-sdk/src')
+ }
+ },
test: {
environment: 'jsdom',
include: ['src/**/*.spec.{ts,tsx}'],
diff --git a/zeppelin-web-angular/projects/zeppelin-react/webpack.config.js
b/zeppelin-web-angular/projects/zeppelin-react/webpack.config.js
index aef55f29ac..65a635b8b2 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/webpack.config.js
+++ b/zeppelin-web-angular/projects/zeppelin-react/webpack.config.js
@@ -65,61 +65,19 @@ module.exports = (_env, argv) => {
]
},
plugins: [
+ // No `shared` scope: the shell bundles no React and never calls
container.init.
+ // This remote is the only participant, so there is nothing to dedupe
against. Re-add it once a second exists.
new ModuleFederationPlugin({
name: 'reactApp',
filename: 'remoteEntry.js',
exposes: {
'./PublishedParagraph': './src/pages/PublishedParagraph',
'./ParagraphFooter': './src/components/paragraph/ParagraphFooter'
- },
- shared: {
- react: {
- singleton: true,
- strictVersion: false,
- requiredVersion: '18.3.1',
- eager: true
- },
- 'react-dom': {
- singleton: true,
- strictVersion: false,
- requiredVersion: '18.3.1',
- eager: true
- }
}
}),
new HtmlWebpackPlugin({
template: './src/index.html'
- }),
- {
- apply: compiler => {
- compiler.hooks.afterEmit.tap('GenerateRemoteEntryJson', () => {
- const fs = require('fs');
- const path = require('path');
-
- const remoteEntryJson = {
- name: 'zeppelinReact',
- type: 'module',
- version: '1.0.0',
- baseUrl: isProduction ? '/assets/react/' :
'http://localhost:3001/',
- exposes: {
- './PublishedParagraph': './PublishedParagraph.tsx',
- './ParagraphFooter': './ParagraphFooter.tsx'
- }
- };
-
- const outputDir = path.resolve(__dirname, 'dist');
- const outputPath = path.resolve(outputDir, 'remoteEntry.json');
-
- // Ensure directory exists
- if (!fs.existsSync(outputDir)) {
- fs.mkdirSync(outputDir, { recursive: true });
- }
-
- fs.writeFileSync(outputPath, JSON.stringify(remoteEntryJson, null,
2));
- console.log('Generated remoteEntry.json for Native Federation');
- });
- }
- }
+ })
],
output: {
path: path.resolve(__dirname, 'dist'),
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/package.json
b/zeppelin-web-angular/projects/zeppelin-sdk/package.json
index 09f5b00a54..b8d9e821bf 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/package.json
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/package.json
@@ -5,7 +5,6 @@
"tslib": "^2.0.0"
},
"peerDependencies": {
- "@angular/common": "^8.2.9",
- "@angular/core": "^8.2.9"
+ "rxjs": "^7.0.0"
}
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index 552e0f8c8d..6f5e24974c 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -43,6 +43,7 @@ import {
NgZService,
NoteStatusService,
NoteVarShareService,
+ ReactFeatureService,
SecurityService,
ThemeService,
TicketService
@@ -431,7 +432,8 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
private securityService: SecurityService,
private router: Router,
private titleService: Title,
- private themeService: ThemeService
+ private themeService: ThemeService,
+ private reactFeature: ReactFeatureService
) {
super(messageService);
}
@@ -448,7 +450,7 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
this.activatedRoute.queryParamMap
.pipe(startWith(this.activatedRoute.snapshot.queryParamMap),
takeUntil(this.destroy$))
.subscribe(data => {
- this.useReactFooter = data.get('reactFooter') === 'true';
+ this.useReactFooter = this.reactFeature.isEnabled('paragraphFooter',
data);
this.cdr.markForCheck();
});
this.activatedRoute.params.pipe(takeUntil(this.destroy$),
distinctUntilKeyChanged('noteId')).subscribe(() => {
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
index 78e05a2b2c..aa58141ea6 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
+++
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
@@ -9,10 +9,12 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-@if (useReact) {
- <div #reactContainer></div>
+@if (useReact && !reactFailed) {
+ @if (paragraph) {
+ <div [zeppelin-react-mount]="'./PublishedParagraph'"
[reactProps]="reactProps"></div>
+ }
}
-@if (!useReact) {
+@if (!useReact || reactFailed) {
<div>
@if (paragraph) {
<zeppelin-notebook-paragraph-dynamic-forms
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
index b1b05d44a7..94f9af7d94 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
@@ -13,8 +13,6 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- ElementRef,
- OnDestroy,
QueryList,
TemplateRef,
ViewChild,
@@ -29,13 +27,12 @@ import {
ParagraphItem,
ParagraphIResultsMsgItem
} from '@zeppelin/sdk';
-import { HeliumService, MessageService, NgZService, NoteStatusService } from
'@zeppelin/services';
-import { RemoteContainer } from '@zeppelin/share';
+import { HeliumService, MessageService, NgZService, NoteStatusService,
ReactFeatureService } from '@zeppelin/services';
import { SpellResult } from '@zeppelin/spell';
import { isNil } from 'lodash';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NotebookParagraphResultComponent } from
'../../share/result/result.component';
-import { environment } from '../../../../../environments/environment';
+import { ReactHostCallbacks, ReactProps } from '../../../../share/react-mount';
@Component({
selector: 'zeppelin-publish-paragraph',
@@ -44,20 +41,20 @@ import { environment } from
'../../../../../environments/environment';
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
-export class PublishedParagraphComponent extends ParagraphBase implements
Published, OnDestroy {
+export class PublishedParagraphComponent extends ParagraphBase implements
Published {
readonly [publishedSymbol] = true;
noteId: string | null = null;
paragraphId: string | null = null;
previewCode: string = '';
useReact = false;
+ // Separate from useReact (which is re-derived from the URL) so a failed
remote stays degraded.
+ reactFailed = false;
isLoading = true;
error: string | null = null;
- private unmountReact: (() => void) | null = null;
- private reactScriptLoaded = false;
+ reactProps: ReactProps & ReactHostCallbacks = {};
@ViewChild('codePreviewModal', { static: true }) codePreviewModal!:
TemplateRef<void>;
- @ViewChild('reactContainer', { static: false }) reactContainer!:
ElementRef<HTMLDivElement>;
@ViewChildren(NotebookParagraphResultComponent)
notebookParagraphResultComponents!:
QueryList<NotebookParagraphResultComponent>;
@@ -67,13 +64,14 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
private heliumService: HeliumService,
private router: Router,
private nzModalService: NzModalService,
+ private reactFeature: ReactFeatureService,
noteStatusService: NoteStatusService,
ngZService: NgZService,
cdr: ChangeDetectorRef
) {
super(messageService, noteStatusService, ngZService, cdr);
- this.activatedRoute.queryParams.subscribe(queryParams => {
- this.useReact = queryParams.react === 'true' || queryParams.react === '';
+ this.activatedRoute.queryParamMap.subscribe(params => {
+ this.useReact = this.reactFeature.isEnabled('publishedParagraph',
params);
});
this.activatedRoute.params.subscribe(params => {
@@ -86,12 +84,6 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
});
}
- ngOnDestroy() {
- if (this.useReact) {
- this.cleanupReactWidget();
- }
- }
-
@MessageListener(OP.NOTE)
getNote(data: MessageReceiveDataTypeMap[OP.NOTE]) {
const note = data.note;
@@ -101,11 +93,11 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
if (!this.paragraph.results) {
this.showRunConfirmationModal();
}
- if (this.useReact) {
+ if (this.useReact && !this.reactFailed) {
this.setResults(this.paragraph);
+ this.reactProps = this.buildReactProps(this.paragraph);
this.isLoading = false;
this.cdr.markForCheck();
- this.loadReactWidget();
return;
}
@@ -161,12 +153,24 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
}
updateParagraphResult(resultIndex: number, config: ParagraphConfigResult,
result: ParagraphIResultsMsgItem): void {
+ // In React mode the Angular result components never render, so this query
is empty.
+ // The remote is refreshed from updateParagraphObjectWhenUpdated instead.
const resultComponent =
this.notebookParagraphResultComponents.toArray()[resultIndex];
if (resultComponent) {
resultComponent.updateResult(config, result);
}
}
+ updateParagraphObjectWhenUpdated(newPara: ParagraphItem): void {
+ super.updateParagraphObjectWhenUpdated(newPara);
+ // A run pushes OP.PARAGRAPH, not OP.NOTE, so getNote does not rebuild the
props.
+ // Do it here, once the base class has merged the new results into
this.paragraph.
+ if (this.useReact && !this.reactFailed && this.paragraph) {
+ this.reactProps = this.buildReactProps(this.paragraph);
+ this.cdr.markForCheck();
+ }
+ }
+
private showRunConfirmationModal(): void {
if (!this.paragraph) {
return;
@@ -204,75 +208,25 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
});
}
- /**
- * Loads the React micro-frontend via Webpack Module Federation.
- *
- * Flow:
- * 1. Loads remoteEntry.js once (skips on subsequent calls via
`reactScriptLoaded` flag).
- * 2. remoteEntry.js registers `window.reactApp` as a federation container.
- * 3. `container.get('./PublishedParagraph')` returns a module with a
`mount(el, props)` function.
- * 4. `mount()` calls `createRoot()` and renders into the given element.
- * 5. `mount()` returns an `unmount` function, stored for cleanup in
`ngOnDestroy`.
- *
- * See `projects/zeppelin-react/README.md` for the full guide.
- * Append `?react=true` to a published paragraph URL to activate.
- */
- private loadReactWidget() {
- if (!this.reactContainer || !this.paragraph) {
- return;
- }
-
- const loadModule = async () => {
- const container: RemoteContainer | undefined = window.reactApp;
- if (!container) {
- throw new Error('window.reactApp not available');
+ private buildReactProps(paragraph: ParagraphItem): ReactProps &
ReactHostCallbacks {
+ return {
+ paragraphId: this.paragraphId,
+ noteId: this.noteId,
+ results: paragraph.results?.msg,
+ config: paragraph.config?.results,
+ onError: (err: unknown) => {
+ console.error('[PublishedParagraph] React mount failed', err);
+ this.degradeToAngular(paragraph);
}
-
- const factory = await container.get<{ mount: (el: HTMLElement, props:
unknown) => () => void }>(
- './PublishedParagraph'
- );
- const { mount } = factory();
-
- if (!mount || typeof mount !== 'function') {
- throw new Error('mount function not found');
- }
-
- const mountPoint = this.reactContainer.nativeElement;
- const props = {
- paragraphId: this.paragraphId,
- noteId: this.noteId,
- results: this.paragraph?.results?.msg,
- config: this.paragraph?.config?.results
- };
-
- this.unmountReact = mount(mountPoint, props);
- };
-
- if (this.reactScriptLoaded) {
- loadModule();
- return;
- }
-
- const script = document.createElement('script');
- script.src = environment.reactRemoteEntryUrl;
-
- script.onload = () => {
- this.reactScriptLoaded = true;
- loadModule();
};
-
- script.onerror = () => {
- this.error = 'Failed to load React widget';
- this.cdr.markForCheck();
- };
-
- document.head.appendChild(script);
}
- private cleanupReactWidget() {
- if (this.unmountReact) {
- this.unmountReact();
- this.unmountReact = null;
- }
+ private degradeToAngular(paragraph: ParagraphItem): void {
+ this.reactFailed = true;
+ this.error = 'Failed to load React widget';
+ // The React branch skipped the Angular init path (getNote), so run it
before showing the fallback.
+ this.originalText = paragraph.text;
+ this.initializeDefault(paragraph.config, paragraph.settings);
+ this.cdr.markForCheck();
}
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
b/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
index 236b9b2ac3..902ec430f4 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
@@ -12,12 +12,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { ShareModule } from '@zeppelin/share';
import { WorkspaceShareModule } from '../share/share.module';
import { PublishedParagraphComponent } from './paragraph/paragraph.component';
import { PublishedRoutingModule } from './published-ruoting.module';
@NgModule({
declarations: [PublishedParagraphComponent],
- imports: [CommonModule, WorkspaceShareModule, PublishedRoutingModule]
+ imports: [CommonModule, ShareModule, WorkspaceShareModule,
PublishedRoutingModule]
})
export class PublishedModule {}
diff --git a/zeppelin-web-angular/src/app/services/public-api.ts
b/zeppelin-web-angular/src/app/services/public-api.ts
index a554dc9cf4..7a80b4d351 100644
--- a/zeppelin-web-angular/src/app/services/public-api.ts
+++ b/zeppelin-web-angular/src/app/services/public-api.ts
@@ -27,6 +27,7 @@ export * from './note-status.service';
export * from './note-var-share.service';
export * from './notebook-repos.service';
export * from './notebook.service';
+export * from './react-feature.service';
export * from './runtime-compiler.service';
export * from './save-as.service';
export * from './security.service';
diff --git a/zeppelin-web-angular/src/app/services/react-feature.service.ts
b/zeppelin-web-angular/src/app/services/react-feature.service.ts
new file mode 100644
index 0000000000..0d79bd9e6e
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/react-feature.service.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+
+export type ReactSurface = 'publishedParagraph' | 'paragraphFooter';
+
+interface ReactSurfaceConfig {
+ queryParam: string;
+ defaultEnabled: boolean;
+}
+
+const SURFACES: Record<ReactSurface, ReactSurfaceConfig> = {
+ publishedParagraph: {
+ queryParam: 'react',
+ defaultEnabled: false
+ },
+ paragraphFooter: {
+ queryParam: 'reactFooter',
+ defaultEnabled: false
+ }
+};
+
+/**
+ * Satisfied by Angular's `ParamMap` and by any `Map`.
+ * Taking the source rather than an already-read value keeps the query-param
name in SURFACES only,
+ * so renaming it cannot desync a call site.
+ */
+export interface FlagSource {
+ get(name: string): string | null | undefined;
+}
+
+@Injectable({ providedIn: 'root' })
+export class ReactFeatureService {
+ isEnabled(surface: ReactSurface, source?: FlagSource | null): boolean {
+ const config = SURFACES[surface];
+
+ const fromQuery = this.parseFlag(source?.get(config.queryParam));
+ if (fromQuery !== null) {
+ return fromQuery;
+ }
+
+ return config.defaultEnabled;
+ }
+
+ /**
+ * A bare flag (`?react`) or `=true` enables, `=false` disables. Anything
else, including an absent flag, is unset.
+ */
+ private parseFlag(value: string | null | undefined): boolean | null {
+ if (value === undefined || value === null) {
+ return null;
+ }
+ if (value === 'true' || value === '') {
+ return true;
+ }
+ if (value === 'false') {
+ return false;
+ }
+ return null;
+ }
+}
diff --git
a/zeppelin-web-angular/src/app/share/react-mount/react-mount-handle.ts
b/zeppelin-web-angular/src/app/share/react-mount/react-mount-handle.ts
index aefeb58fe3..1285a0c492 100644
--- a/zeppelin-web-angular/src/app/share/react-mount/react-mount-handle.ts
+++ b/zeppelin-web-angular/src/app/share/react-mount/react-mount-handle.ts
@@ -30,15 +30,3 @@ export type ReactMountFn = (element: HTMLElement, props:
ReactProps & ReactHostC
export interface ReactExposedModule {
mount: ReactMountFn;
}
-
-/**
- * Legacy shape (used by ./PublishedParagraph until its follow-up
- * refactor): mount returns a bare unmount function.
- */
-export type LegacyMountFn = (element: HTMLElement, props: ReactProps) => () =>
void;
-
-export interface LegacyExposedModule {
- mount: LegacyMountFn;
-}
-
-export type AnyExposedModule = ReactExposedModule | LegacyExposedModule;
diff --git
a/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.ts
b/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.ts
index 96ff0ee7d0..f981710bff 100644
--- a/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.ts
+++ b/zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.ts
@@ -12,25 +12,7 @@
import { Directive, ElementRef, Input, NgZone, OnChanges, OnDestroy,
SimpleChanges } from '@angular/core';
import { ReactRemoteLoaderService } from './react-remote-loader.service';
-import {
- AnyExposedModule,
- ReactExposedModule,
- ReactHostCallbacks,
- ReactMountHandle,
- ReactProps
-} from './react-mount-handle';
-
-const isLegacyModule = (mod: AnyExposedModule, handleOrUnmount: unknown):
handleOrUnmount is () => void => {
- void mod;
- return typeof handleOrUnmount === 'function';
-};
-
-const wrapLegacyHandle = (unmount: () => void): ReactMountHandle => ({
- update: () => {
- /* legacy modules don't support updates; no-op */
- },
- unmount
-});
+import { ReactExposedModule, ReactHostCallbacks, ReactMountHandle, ReactProps
} from './react-mount-handle';
@Directive({
selector: '[zeppelin-react-mount]',
@@ -101,18 +83,13 @@ export class ReactMountDirective implements OnChanges,
OnDestroy {
this.loading = true;
const moduleKey = this.module;
try {
- const mod = await this.loader.loadModule<AnyExposedModule>(moduleKey);
+ const mod = await this.loader.loadModule<ReactExposedModule>(moduleKey);
if (this.destroyed) {
return;
}
this.ngZone.runOutsideAngular(() => {
try {
- const returned = (mod as
ReactExposedModule).mount(this.host.nativeElement, this.latestProps);
- if (isLegacyModule(mod, returned)) {
- this.handle = wrapLegacyHandle(returned as unknown as () => void);
- } else {
- this.handle = returned as ReactMountHandle;
- }
+ this.handle = mod.mount(this.host.nativeElement, this.latestProps);
this.mountedModule = moduleKey;
} catch (err) {
this.handle = null;
diff --git
a/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
b/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
index 2c903f529a..7e992dc2d4 100644
---
a/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
+++
b/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
@@ -12,9 +12,9 @@
import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';
-import { AnyExposedModule } from './react-mount-handle';
+import { ReactExposedModule } from './react-mount-handle';
-export interface RemoteContainer {
+interface RemoteContainer {
get<T>(key: string): Promise<() => T>;
init?: (shareScope: unknown) => Promise<void>;
}
@@ -28,7 +28,7 @@ declare global {
@Injectable({ providedIn: 'root' })
export class ReactRemoteLoaderService {
private containerPromise: Promise<RemoteContainer> | null = null;
- private readonly modulePromises = new Map<string,
Promise<AnyExposedModule>>();
+ private readonly modulePromises = new Map<string,
Promise<ReactExposedModule>>();
loadContainer(): Promise<RemoteContainer> {
if (this.containerPromise) {
@@ -90,7 +90,7 @@ export class ReactRemoteLoaderService {
return this.containerPromise;
}
- loadModule<T extends AnyExposedModule>(exposedKey: string): Promise<T> {
+ loadModule<T extends ReactExposedModule>(exposedKey: string): Promise<T> {
const cached = this.modulePromises.get(exposedKey);
if (cached) {
return cached as Promise<T>;
diff --git a/zeppelin-web-angular/webpack.config.js
b/zeppelin-web-angular/webpack.config.js
index 0e73411a6a..03379409c8 100644
--- a/zeppelin-web-angular/webpack.config.js
+++ b/zeppelin-web-angular/webpack.config.js
@@ -11,8 +11,6 @@
*/
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
-const webpack = require('@angular-devkit/build-angular/node_modules/webpack');
-const ModuleFederationPlugin = webpack.container.ModuleFederationPlugin;
const MONACO_DIR = /monaco-editor[\\/]/;
@@ -72,14 +70,6 @@ module.exports = (config, options, targetOptions) => {
});
config.plugins = config.plugins || [];
- config.plugins.push(
- new ModuleFederationPlugin({
- name: 'shell',
- remotes: {
- reactApp: 'reactApp@http://localhost:3001/remoteEntry.js'
- }
- })
- );
config.plugins.push(
new MonacoWebpackPlugin({
languages: [