diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts index 13293c528e5..e63350da022 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 , so tests that only assert "React took over" accept either. + this.reactWidgetOrEmptyState = this.reactWidget.or(page.locator('.ant-alert')); } async navigateToNotebook(noteId: string): Promise { 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 fd102b36d4d..fc41603837b 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 099212243d3..fc9dce1f6fa 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
or (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 6e5e275dbd2..582dfacab68 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", @@ -77,7 +76,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", @@ -312,36 +310,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", @@ -612,15 +580,15 @@ } }, "node_modules/@angular-builders/custom-webpack/node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "optional": true, "peer": true, "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@angular-builders/custom-webpack/node_modules/@vitejs/plugin-basic-ssl": { @@ -1217,15 +1185,15 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "optional": true, "peer": true, "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/agent-base": { @@ -1642,23 +1610,6 @@ "typescript": "*" } }, - "node_modules/@angular/animations": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.15.tgz", - "integrity": "sha512-Z8AsLTwc++Fcu0fJnclAF9zMfumAd5KXrwtSdyECqLpqd+lEmmsOpeOl6P7loqdDz99KYh/8UF4eJxdMvnsaKw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/core": "21.2.15" - } - }, "node_modules/@angular/cdk": { "version": "21.2.13", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.13.tgz", @@ -2082,15 +2033,15 @@ } }, "node_modules/@angular/cli/node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "optional": true, "peer": true, "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@angular/cli/node_modules/ansi-regex": { @@ -5842,278 +5793,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", @@ -6521,20 +6200,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, "node_modules/@ngtools/webpack": { "version": "21.2.13", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-21.2.13.tgz", @@ -7970,273 +7635,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@rspack/binding": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", - "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", - "dev": true, - "license": "MIT", - "peer": true, - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.11", - "@rspack/binding-darwin-x64": "1.7.11", - "@rspack/binding-linux-arm64-gnu": "1.7.11", - "@rspack/binding-linux-arm64-musl": "1.7.11", - "@rspack/binding-linux-x64-gnu": "1.7.11", - "@rspack/binding-linux-x64-musl": "1.7.11", - "@rspack/binding-wasm32-wasi": "1.7.11", - "@rspack/binding-win32-arm64-msvc": "1.7.11", - "@rspack/binding-win32-ia32-msvc": "1.7.11", - "@rspack/binding-win32-x64-msvc": "1.7.11" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", - "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", - "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", - "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", - "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", - "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", - "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", - "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@napi-rs/wasm-runtime": "1.0.7" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", - "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", - "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", - "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/core": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", - "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.11", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/error-codes": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", - "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", - "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/runtime-core": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime-core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", - "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", - "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/webpack-bundler-runtime": "0.22.0" - } - }, - "node_modules/@rspack/core/node_modules/@module-federation/sdk": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", - "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", - "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@rspack/lite-tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", - "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -8630,15 +8028,7 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "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 + "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.1", @@ -9256,17 +8646,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", @@ -10166,15 +9545,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", @@ -10496,17 +9866,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", @@ -10921,20 +10280,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", @@ -11254,16 +10599,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-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -12896,20 +12231,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/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -13055,30 +12376,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", @@ -13144,34 +12441,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", @@ -13380,19 +12649,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", @@ -13681,62 +12937,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", @@ -13930,20 +13130,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", @@ -14984,17 +14170,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", @@ -15034,17 +14209,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", @@ -15087,17 +14251,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -15776,14 +14929,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", @@ -15803,17 +14948,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", @@ -16593,21 +15727,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", @@ -16616,27 +15735,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", @@ -16666,25 +15764,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", @@ -16772,22 +15851,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", @@ -17422,17 +16485,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", @@ -18367,21 +17419,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", @@ -19432,14 +18469,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", @@ -20508,21 +19537,10 @@ "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": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT", "optional": true, @@ -20582,18 +19600,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", @@ -20746,16 +19752,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/webpack": { "version": "5.107.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", diff --git a/zeppelin-web-angular/package.json b/zeppelin-web-angular/package.json index 756dab31275..f9d948b49a4 100644 --- a/zeppelin-web-angular/package.json +++ b/zeppelin-web-angular/package.json @@ -70,7 +70,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", @@ -104,7 +103,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 f452ee1455a..b258c895e3a 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/README.md +++ b/zeppelin-web-angular/projects/zeppelin-react/README.md @@ -44,22 +44,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('')` 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 @@ -137,7 +152,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 4fa79386ebe..4b66e7d0ba4 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": { @@ -8862,6 +8862,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", @@ -9984,7 +9993,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 3cca715db9c..3dd2c0e0ced 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 00000000000..0999b7a71e2 --- /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 2d48314d5c1..e95cc928f8b 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/pages/PublishedParagraph.tsx @@ -10,9 +10,9 @@ * limitations under the License. */ -import { createRoot } from 'react-dom/client'; +import { createRoot, Root } from 'react-dom/client'; import { ConfigProvider } from 'antd'; -import { Empty } from '@/components'; +import { Empty, ReactErrorBoundary } from '@/components'; import { SingleResultRenderer } from '@/templates'; import type { ParagraphConfigResults, ParagraphIResultsMsgItem } from '@zeppelin/sdk'; @@ -20,6 +20,7 @@ export interface PublishedParagraphProps { paragraphId: string; results?: ParagraphIResultsMsgItem[]; config?: ParagraphConfigResults; + onError?: (error: unknown) => void; } export const PublishedParagraph = ({ results, config }: PublishedParagraphProps) => { @@ -46,22 +47,38 @@ export const PublishedParagraph = ({ results, config }: PublishedParagraphProps) ); }; -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); - root.render( - - ); + const renderWith = (props?: PublishedParagraphProps) => { + root.render( + + + + ); + }; + + 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 8fde66b75c1..ffddd37ae3e 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 aef55f29ace..65a635b8b27 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 09f5b00a54e..b8d9e821bfa 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 552e0f8c8d4..6f5e24974c8 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 78e05a2b2cd..aa58141ea68 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) { -
+@if (useReact && !reactFailed) { + @if (paragraph) { +
+ } } -@if (!useReact) { +@if (!useReact || reactFailed) {
@if (paragraph) { void) | null = null; - private reactScriptLoaded = false; + reactProps: ReactProps & ReactHostCallbacks = {}; @ViewChild('codePreviewModal', { static: true }) codePreviewModal!: TemplateRef; - @ViewChild('reactContainer', { static: false }) reactContainer!: ElementRef; @ViewChildren(NotebookParagraphResultComponent) notebookParagraphResultComponents!: QueryList; @@ -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 236b9b2ac3a..902ec430f49 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 a554dc9cf43..7a80b4d351d 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 00000000000..0d79bd9e6ea --- /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 = { + 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 aefeb58fe31..1285a0c492e 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 c93c168001b..cf3b6a72f01 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]', @@ -99,18 +81,13 @@ export class ReactMountDirective implements OnChanges, OnDestroy { this.loading = true; const moduleKey = this.module; try { - const mod = await this.loader.loadModule(moduleKey); + const mod = await this.loader.loadModule(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 c3f45911ea5..94e391918f0 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(key: string): Promise<() => T>; init?: (shareScope: unknown) => Promise; } @@ -28,7 +28,7 @@ declare global { @Injectable({ providedIn: 'root' }) export class ReactRemoteLoaderService { private containerPromise: Promise | null = null; - private readonly modulePromises = new Map>(); + private readonly modulePromises = new Map>(); loadContainer(): Promise { if (this.containerPromise) { @@ -74,7 +74,7 @@ export class ReactRemoteLoaderService { return this.containerPromise; } - loadModule(exposedKey: string): Promise { + loadModule(exposedKey: string): Promise { const cached = this.modulePromises.get(exposedKey); if (cached) { return cached as Promise; diff --git a/zeppelin-web-angular/webpack.config.js b/zeppelin-web-angular/webpack.config.js index 0e73411a6a1..03379409c82 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: [