diff --git a/frontend/LICENSE-binary b/frontend/LICENSE-binary index 8e3904cadb7..2759b6f512d 100644 --- a/frontend/LICENSE-binary +++ b/frontend/LICENSE-binary @@ -211,6 +211,7 @@ Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Angular / npm packages: + - dompurify@3.3.1 - fuse.js@6.5.3 - jschardet@3.1.3 - rxjs@7.8.1 @@ -247,30 +248,40 @@ Angular / npm packages: - @ant-design/icons-angular@21.0.0 - @auth0/angular-jwt@5.1.0 - @babel/runtime@7.29.2 - - @codingame/monaco-vscode-api@8.0.4 - - @codingame/monaco-vscode-base-service-override@8.0.4 - - @codingame/monaco-vscode-configuration-service-override@8.0.4 - - @codingame/monaco-vscode-editor-api@8.0.4 - - @codingame/monaco-vscode-environment-service-override@8.0.4 - - @codingame/monaco-vscode-extensions-service-override@8.0.4 - - @codingame/monaco-vscode-files-service-override@8.0.4 - - @codingame/monaco-vscode-host-service-override@8.0.4 - - @codingame/monaco-vscode-java-default-extension@8.0.4 - - @codingame/monaco-vscode-languages-service-override@8.0.4 - - @codingame/monaco-vscode-layout-service-override@8.0.4 - - @codingame/monaco-vscode-model-service-override@8.0.4 - - @codingame/monaco-vscode-monarch-service-override@8.0.4 - - @codingame/monaco-vscode-python-default-extension@8.0.4 - - @codingame/monaco-vscode-quickaccess-service-override@8.0.4 - - @codingame/monaco-vscode-r-default-extension@8.0.4 - - @codingame/monaco-vscode-textmate-service-override@8.0.4 - - @codingame/monaco-vscode-theme-defaults-default-extension@8.0.4 - - @codingame/monaco-vscode-theme-service-override@8.0.4 + - @codingame/monaco-vscode-api@25.1.2 + - @codingame/monaco-vscode-base-service-override@25.1.2 + - @codingame/monaco-vscode-bulk-edit-service-override@25.1.2 + - @codingame/monaco-vscode-configuration-service-override@25.1.2 + - @codingame/monaco-vscode-editor-api@25.1.2 + - @codingame/monaco-vscode-editor-service-override@25.1.2 + - @codingame/monaco-vscode-environment-service-override@25.1.2 + - @codingame/monaco-vscode-extension-api@25.1.2 + - @codingame/monaco-vscode-extensions-service-override@25.1.2 + - @codingame/monaco-vscode-files-service-override@25.1.2 + - @codingame/monaco-vscode-host-service-override@25.1.2 + - @codingame/monaco-vscode-java-default-extension@25.1.2 + - @codingame/monaco-vscode-keybindings-service-override@25.1.2 + - @codingame/monaco-vscode-languages-service-override@25.1.2 + - @codingame/monaco-vscode-layout-service-override@25.1.2 + - @codingame/monaco-vscode-log-service-override@25.1.2 + - @codingame/monaco-vscode-model-service-override@25.1.2 + - @codingame/monaco-vscode-monarch-service-override@25.1.2 + - @codingame/monaco-vscode-python-default-extension@25.1.2 + - @codingame/monaco-vscode-quickaccess-service-override@25.1.2 + - @codingame/monaco-vscode-textmate-service-override@25.1.2 + - @codingame/monaco-vscode-theme-defaults-default-extension@25.1.2 + - @codingame/monaco-vscode-theme-service-override@25.1.2 + - @codingame/monaco-vscode-view-banner-service-override@25.1.2 + - @codingame/monaco-vscode-view-common-service-override@25.1.2 + - @codingame/monaco-vscode-view-status-bar-service-override@25.1.2 + - @codingame/monaco-vscode-view-title-bar-service-override@25.1.2 + - @codingame/monaco-vscode-views-service-override@25.1.2 + - @codingame/monaco-vscode-workbench-service-override@25.1.2 - @ctrl/tinycolor@3.6.1 - @ngneat/until-destroy@8.1.4 - @ngx-formly/core@6.3.12 - @ngx-formly/ng-zorro-antd@6.3.12 - - @vscode/iconv-lite-umd@0.7.0 + - @vscode/iconv-lite-umd@0.7.1 - ajv@8.10.0 - backbone@1.4.1 - balanced-match@1.0.2 @@ -283,18 +294,16 @@ Angular / npm packages: - file-saver@2.0.5 - graphlib@2.1.8 - html2canvas@1.4.1 - - java@1.0.0 - jquery@3.6.4 - json-schema-traverse@1.0.0 - jszip@3.10.1 - lib0@0.2.117 - lodash@4.18.1 - lodash-es@4.18.1 - - marked@17.0.1 + - marked@14.0.0 - mobx@4.14.1 - monaco-breakpoints@0.2.0 - - monaco-editor-wrapper@5.5.3 - - monaco-languageclient@8.8.3 + - monaco-languageclient@10.7.0 - ng-zorro-antd@21.2.2 - ngx-color-picker@12.0.1 - ngx-file-drop@16.0.0 @@ -303,23 +312,18 @@ Angular / npm packages: - papaparse@5.4.1 - plotly.js-basic-dist-min@2.29.0 - point-in-polygon@1.1.0 - - python@1.0.0 - quill-cursors@3.1.2 - - r@1.0.0 - rbush@4.0.1 - read-excel-file@5.7.1 - ring-buffer-ts@1.0.3 - style-loader@3.3.4 - - theme-defaults@1.0.0 - underscore@1.13.8 - uuid@8.3.2 - vscode-jsonrpc@8.2.0 - vscode-languageclient@9.0.1 - vscode-languageserver-protocol@3.17.5 - vscode-languageserver-types@3.17.5 - - vscode-oniguruma@1.7.0 - - vscode-textmate@9.0.0 - - vscode-ws-jsonrpc@3.3.2 + - vscode-ws-jsonrpc@3.5.0 - y-monaco@0.1.5 - y-protocols@1.0.7 - y-quill@0.1.5 diff --git a/frontend/custom-webpack.config.js b/frontend/custom-webpack.config.js index 47a804cf435..1ee52633aa5 100644 --- a/frontend/custom-webpack.config.js +++ b/frontend/custom-webpack.config.js @@ -17,46 +17,78 @@ * under the License. */ +const path = require("path"); const { LicenseWebpackPlugin } = require("license-webpack-plugin"); +const nodeModule = (...segments) => path.resolve(__dirname, "node_modules", ...segments); +const codingameCssRe = /node_modules[\\/](?:@codingame[\\/]monaco-vscode-[^\\/]+|monaco-editor|vscode)[\\/].*\.css$/; + module.exports = { module: { rules: [ + { + // codingame monaco-vscode-* ships raw assets (svg/ttf/png/woff*) that + // webpack must emit as static files rather than try to parse as JS. + test: /\.(svg|ttf|woff2?|png|jpg|jpeg|gif)$/, + include: [nodeModule("@codingame")], + type: "asset/resource", + }, { test: /\.css$/, - use: ["style-loader", "css-loader"], - include: [ - require("path").resolve(__dirname, "node_modules/monaco-editor"), - require("path").resolve(__dirname, "node_modules/monaco-breakpoints") + oneOf: [ + { + // codingame monaco-vscode-* CSS ships as Constructable Stylesheet + // imports — must skip style-loader and use css-loader's + // `exportType: 'css-style-sheet'`. + // https://github.com/CodinGame/monaco-vscode-api/wiki/Troubleshooting + test: codingameCssRe, + use: [ + { + loader: "css-loader", + options: { esModule: false, exportType: "css-style-sheet", url: true, import: true }, + }, + ], + }, + { + // monaco-breakpoints ships a plain stylesheet that needs + // style-loader so it injects at runtime. + include: [nodeModule("monaco-breakpoints")], + use: ["style-loader", "css-loader"], + }, ], }, ], - // Enable URL handling in webpack's JavaScript parser, required for loading .wasm files. - // See https://github.com/angular/angular-cli/issues/24617 - parser: { - javascript: { - url: true, - }, + }, + resolve: { + // css-loader emits relative imports (e.g. '../../../../../../../css-loader/ + // dist/runtime/api.js') computed from the source CSS location. The codingame + // monaco-vscode-* packages live one namespace level deeper (under + // `node_modules/@codingame/...`) than css-loader assumes, so the emitted + // path lands at `node_modules/@codingame/css-loader/...` instead of + // `node_modules/css-loader/...`. Alias the missing leg back to the real + // install so webpack can resolve the runtime files. + alias: { + [nodeModule("@codingame/css-loader")]: nodeModule("css-loader"), + [nodeModule("@codingame/style-loader")]: nodeModule("style-loader"), }, }, plugins: [ new LicenseWebpackPlugin({ perChunkOutput: false, outputFilename: "3rdpartylicenses.json", + // Some codingame monaco-vscode-* sub-modules don't expose a license file + // license-webpack-plugin can find; treat that as soft instead of fatal. + handleMissingLicenseText: () => null, renderLicenses: (modules) => JSON.stringify( modules + .filter((m) => m.packageJson?.name && m.packageJson?.version) .map((m) => ({ - name: m.packageJson && m.packageJson.name, - version: m.packageJson && m.packageJson.version, + name: m.packageJson.name, + version: m.packageJson.version, license: m.licenseId, })) - .filter((e) => e.name && e.version) - .sort((a, b) => - a.name === b.name - ? a.version.localeCompare(b.version) - : a.name.localeCompare(b.name), - ), + .sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)), null, 2, ), diff --git a/frontend/package.json b/frontend/package.json index 08b298260e3..b69997501fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,6 @@ "start": "concurrently --kill-others \"npx y-websocket\" \"ng serve\"", "build": "node --max-old-space-size=8192 ./node_modules/@angular/cli/bin/ng build --configuration=production --progress=false --source-map=false", "build:ci": "node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js build --configuration=production --progress=false --source-map=false", - "analyze": "ng build --configuration=production --stats-json && webpack-bundle-analyzer dist/stats.json", "test": "ng test --watch=false", "test:ci": "node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js test --watch=false --progress=false --coverage --coverage-reporters=lcovonly", "prettier:fix": "prettier --write ./src", @@ -35,9 +34,8 @@ "@angular/platform-browser-dynamic": "21.2.10", "@angular/router": "21.2.10", "@auth0/angular-jwt": "5.1.0", - "@codingame/monaco-vscode-java-default-extension": "8.0.4", - "@codingame/monaco-vscode-python-default-extension": "8.0.4", - "@codingame/monaco-vscode-r-default-extension": "8.0.4", + "@codingame/monaco-vscode-java-default-extension": "25.1.2", + "@codingame/monaco-vscode-python-default-extension": "25.1.2", "@ngneat/until-destroy": "8.1.4", "@ngx-formly/core": "6.3.12", "@ngx-formly/ng-zorro-antd": "6.3.12", @@ -54,9 +52,8 @@ "lodash-es": "4.18.1", "marked": "17.0.1", "monaco-breakpoints": "0.2.0", - "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@8.0.4", - "monaco-editor-wrapper": "5.5.3", - "monaco-languageclient": "8.8.3", + "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@25.1.2", + "monaco-languageclient": "10.7.0", "ng-zorro-antd": "21.2.2", "ngx-color-picker": "12.0.1", "ngx-file-drop": "16.0.0", @@ -73,7 +70,6 @@ "tinyqueue": "2.0.3", "tslib": "2.3.1", "uuid": "8.3.2", - "vscode": "npm:@codingame/monaco-vscode-api@8.0.4", "y-monaco": "0.1.5", "y-protocols": "1.0.5", "y-quill": "0.1.5", @@ -83,8 +79,7 @@ "zone.js": "0.15.1" }, "resolutions": { - "vscode": "npm:@codingame/monaco-vscode-api@8.0.4", - "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@8.0.4", + "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@25.1.2", "webpack": "5.104.1", "jschardet": "portal:./tools/jschardet-stub" }, @@ -137,8 +132,7 @@ "sass": "1.71.1", "ts-proto": "2.2.0", "typescript": "5.9.3", - "vitest": "4.1.5", - "webpack-bundle-analyzer": "4.5.0" + "vitest": "4.1.5" }, "browserslist": [ "defaults", diff --git a/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts b/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts index 14384524cfa..d46b19f7340 100644 --- a/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts +++ b/frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts @@ -20,9 +20,6 @@ import { AfterViewInit, Component, Input, ViewChild } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SafeStyle } from "@angular/platform-browser"; -import "@codingame/monaco-vscode-python-default-extension"; -import "@codingame/monaco-vscode-r-default-extension"; -import "@codingame/monaco-vscode-java-default-extension"; import { isDefined } from "../../../common/util/predicate"; import * as monaco from "monaco-editor"; import { MonacoBreakpoint } from "monaco-breakpoints"; diff --git a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts index b477ff59792..0403dd39feb 100644 --- a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts +++ b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts @@ -31,10 +31,10 @@ import { OperatorSchema } from "../../types/operator-schema.interface"; import { of } from "rxjs"; // Operator types that the constructor's language-detection branch must map -// to a specific language. `RUDFSource` / `RUDF` -> `r`; the three V2 Python -// types -> `python`; everything else -> `java`. Local to this spec so we -// don't perturb the shared mock-workflow-data fixtures. -const R_OPERATOR_TYPES = ["RUDFSource", "RUDF"]; +// to a specific language. The three V2 Python types -> `python`; everything +// else (including the legacy `RUDF*` types) -> `java`, since R UDF editor +// support was retired in this branch. Local to this spec so we don't +// perturb the shared mock-workflow-data fixtures. const PYTHON_OPERATOR_TYPES = ["PythonUDFV2", "PythonUDFSourceV2", "DualInputPortsPythonUDFV2"]; // Augment `mockOperatorMetaData` with synthetic schemas for the V2 operator @@ -52,7 +52,6 @@ const synthesizeSchema = (operatorType: string): OperatorSchema => ({ ...baseSch const augmentedSchemas: OperatorSchema[] = [ ...mockOperatorMetaData.operators, ...PYTHON_OPERATOR_TYPES.map(synthesizeSchema), - ...R_OPERATOR_TYPES.map(synthesizeSchema), synthesizeSchema("SomeUnknownType"), ]; class AugmentedStubMetadataService extends StubOperatorMetadataService { @@ -117,19 +116,11 @@ describe("CodeEditorComponent", () => { expect(fixture.componentInstance.currentOperatorId).toBe(mockJavaUDFPredicate.operatorID); }); - // Language detection — the constructor maps `RUDFSource` / `RUDF` to `r`, - // the three V2-era Python operator types to `python`, and anything else - // to `java`. The exact branch lives in the constructor; the public - // `language` field is what the rest of the editor (LSP wiring, file- - // suffix selection) keys off. - - R_OPERATOR_TYPES.forEach((operatorType, index) => { - it(`picks language="r" for operatorType=${operatorType}`, () => { - const fixture = makeFixture(buildPredicate(`r-${index}`, operatorType)); - expect(fixture.componentInstance.language).toBe("r"); - expect(fixture.componentInstance.languageTitle).toBe("R UDF"); - }); - }); + // Language detection — the constructor maps the three V2-era Python + // operator types to `python`, and anything else (including the legacy + // `RUDF*` types that this branch retired) to `java`. The exact branch + // lives in the constructor; the public `language` field is what the + // rest of the editor (LSP wiring, file-suffix selection) keys off. PYTHON_OPERATOR_TYPES.forEach((operatorType, index) => { it(`picks language="python" for operatorType=${operatorType}`, () => { diff --git a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index a0838e6e459..4cc23b0cfd1 100644 --- a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -33,7 +33,7 @@ import { WorkflowVersionService } from "../../../dashboard/service/user/workflow import type { Text as YText } from "yjs"; import { getWebsocketUrl } from "src/app/common/util/url"; import { MonacoBinding } from "y-monaco"; -import { catchError, from, of, Subject, take, timeout } from "rxjs"; +import { from, Subject, take } from "rxjs"; import { CoeditorPresenceService } from "../../service/workflow-graph/model/coeditor-presence.service"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser"; import { Coeditor } from "../../../common/type/user"; @@ -41,13 +41,16 @@ import { YType } from "../../types/shared-editing.interface"; import { FormControl } from "@angular/forms"; import { AIAssistantService, TypeAnnotationResponse } from "../../service/ai-assistant/ai-assistant.service"; import { AnnotationSuggestionComponent } from "./annotation-suggestion.component"; -import { MonacoEditorLanguageClientWrapper, UserConfig } from "monaco-editor-wrapper"; import * as monaco from "monaco-editor"; -import "@codingame/monaco-vscode-python-default-extension"; -import "@codingame/monaco-vscode-r-default-extension"; -import "@codingame/monaco-vscode-java-default-extension"; +import { + MonacoVscodeApiWrapper, + type MonacoVscodeApiConfig, + getEnhancedMonacoEnvironment, +} from "monaco-languageclient/vscodeApiWrapper"; +import { LanguageClientWrapper, type LanguageClientConfig } from "monaco-languageclient/lcwrapper"; +import { EditorApp, type EditorAppConfig } from "monaco-languageclient/editorApp"; import { isDefined } from "../../../common/util/predicate"; -import { filter, switchMap } from "rxjs/operators"; +import { filter } from "rxjs/operators"; import { BreakpointConditionInputComponent } from "./breakpoint-condition-input/breakpoint-condition-input.component"; import { CodeDebuggerComponent } from "./code-debugger.component"; import { GuiConfigService } from "src/app/common/service/gui-config.service"; @@ -105,7 +108,9 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public language: string = ""; public languageTitle: string = ""; - private editorWrapper: MonacoEditorLanguageClientWrapper = new MonacoEditorLanguageClientWrapper(); + private static apiWrapperStartPromise?: Promise; + private editorApp?: EditorApp; + private languageClientWrapper?: LanguageClientWrapper; private monacoBinding?: MonacoBinding; // Boolean to determine whether the suggestion UI should be shown @@ -124,14 +129,11 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public codeDebuggerComponent!: Type | null; public editorToPass!: MonacoEditor; - private generateLanguageTitle(language: string): string { - return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`; - } - - setLanguage(newLanguage: string) { - this.language = newLanguage; - this.languageTitle = this.generateLanguageTitle(newLanguage); - } + private static readonly PYTHON_OPERATOR_TYPES: ReadonlySet = new Set([ + "PythonUDFV2", + "PythonUDFSourceV2", + "DualInputPortsPythonUDFV2", + ]); constructor( private sanitizer: DomSanitizer, @@ -143,18 +145,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy ) { this.currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; const operatorType = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType; - - if (operatorType === "RUDFSource" || operatorType === "RUDF") { - this.setLanguage("r"); - } else if ( - operatorType === "PythonUDFV2" || - operatorType === "PythonUDFSourceV2" || - operatorType === "DualInputPortsPythonUDFV2" - ) { - this.setLanguage("python"); - } else { - this.setLanguage("java"); - } + this.language = CodeEditorComponent.PYTHON_OPERATOR_TYPES.has(operatorType) ? "python" : "java"; + this.languageTitle = `${this.language[0].toUpperCase()}${this.language.slice(1)} UDF`; this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("editingCode", true); this.title = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).customDisplayName; this.code = ( @@ -187,45 +179,113 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("editingCode", false); localStorage.setItem(this.currentOperatorId, this.containerElement.nativeElement.style.cssText); - if (isDefined(this.monacoBinding)) { - this.monacoBinding.destroy(); - } - - this.editorWrapper.dispose(true); + this.monacoBinding?.destroy(); + this.languageClientWrapper?.dispose().catch(() => {}); + this.languageClientWrapper = undefined; + this.editorApp?.dispose().catch(() => {}); + this.editorApp = undefined; - if (isDefined(this.workflowVersionStreamSubject)) { - this.workflowVersionStreamSubject.next(); - this.workflowVersionStreamSubject.complete(); - } + this.workflowVersionStreamSubject.next(); + this.workflowVersionStreamSubject.complete(); } /** * Specify the co-editor's cursor style. This step is missing from MonacoBinding. + * + * `coeditor.clientId` and `coeditor.color` come from yjs awareness state, + * which any peer can write to. Both are interpolated into a `"; - return this.sanitizer.bypassSecurityTrustHtml(textCSS); + `.yRemoteSelection-${id} { background-color: ${selectionBg}}` + + `.yRemoteSelectionHead-${id}::after { border-color: ${color}}` + + `.yRemoteSelectionHead-${id} { border-color: ${color}}` + + "" + ); } - private getFileSuffixByLanguage(language: string): string { - switch (language.toLowerCase()) { - case "python": - return ".py"; - case "r": - return ".r"; - case "javascript": - return ".js"; - case "java": - return ".java"; - default: - return ".py"; - } + // Allow-lists for the two awareness-derived values that flow into a `