chore: remove xml parser v3 as a default library (#28012)

## Description
Contains the changes to remove fast-xml-parserV3.17.5 as a default
library and migrate all existing apps to install it as a custom JS
library on page load. Installations no longer fail when there is a
naming collision, we determine a unique accessor that can work inside
the application both for UMD & ESM builds.

#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith-ee/issues/2562
Fixes https://github.com/appsmithorg/appsmith-ee/issues/2563
Fixes https://github.com/appsmithorg/appsmith-ee/issues/2073
Fixes https://github.com/appsmithorg/appsmith-ee/issues/2403

#### Type of change
- Chore (housekeeping or task changes that don't impact user perception)
>

## Testing
>
#### How Has This Been Tested?
- [x] Manual
- [x] JUnit
- [x] Jest
- [x] Cypress
>
>
#### Test Plan
https://github.com/appsmithorg/TestSmith/issues/2536
Scenarios for existing apps will be tested post-merge since DP's are
created with fresh DB that don't have release data
>
>
#### Issues raised during DP testing

https://github.com/appsmithorg/appsmith/pull/28012#issuecomment-1767711382
response:
https://github.com/appsmithorg/appsmith/pull/28012#issuecomment-1767781029
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [x] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [x] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [x] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [x] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: manish kumar <manish@appsmith.com>
Co-authored-by: Manish Kumar <107841575+sondermanish@users.noreply.github.com>
This commit is contained in:
arunvjn 2023-10-20 11:08:47 +05:30 committed by GitHub
parent 17eae14dfc
commit af9e89d2a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 748 additions and 333 deletions

View File

@ -86,15 +86,6 @@ describe("Autocomplete bug fixes", function () {
);
});
it("6. feat #16426 Autocomplete for fast-xml-parser", function () {
entityExplorer.SelectEntityByName("Text1");
propPane.TypeTextIntoField("Text", "{{xmlParser.j");
agHelper.GetNAssertElementText(locators._hints, "j2xParser");
propPane.TypeTextIntoField("Text", "{{new xmlParser.j2xParser().p");
agHelper.GetNAssertElementText(locators._hints, "parse");
});
it(
"excludeForAirgap",
"7. Installed library should show up in autocomplete",

View File

@ -3,9 +3,21 @@ import * as _ from "../../../../support/Objects/ObjectsCore";
describe("xml2json text", function () {
before(() => {
_.agHelper.AddDsl("xmlParser");
_.homePage.NavigateToHome();
_.homePage.ImportApp("xmlParser.json");
_.homePage.AssertImportToast();
});
it("1. Publish widget and validate the data displayed in text widget from xmlParser function", function () {
it("1. Check if XMLparser v3 autocomplete works", function () {
_.entityExplorer.SelectEntityByName("Text2", "Widgets");
_.propPane.TypeTextIntoField("Text", "{{xmlParser.j", true);
_.agHelper.GetNAssertElementText(_.locators._hints, "j2xParser");
_.propPane.TypeTextIntoField("Text", "{{new xmlParser.j2xParser().p", true);
_.agHelper.GetNAssertElementText(_.locators._hints, "parse");
});
it("2. Publish widget and validate the data displayed in text widget from xmlParser function", function () {
_.deployMode.DeployApp();
cy.get(publish.textWidget)
.first()

View File

@ -53,11 +53,13 @@ describe("Import and validate older app (app created in older versions of Appsmi
gitSync._gitStatusChanges,
/[0-9] page(|s) modified/,
);
agHelper.GetNAssertElementText(
gitSync._gitStatusChanges,
"Application settings modified",
"not.contain.text",
);
// Commenting it as part of #28012 - to be added back later
// agHelper.GetNAssertElementText(
// gitSync._gitStatusChanges,
// "Application settings modified",
// "not.contain.text",
// );
agHelper.GetNAssertElementText(
gitSync._gitStatusChanges,
"Theme modified",
@ -73,7 +75,10 @@ describe("Import and validate older app (app created in older versions of Appsmi
// );
agHelper.AssertContains(/[0-9] JS Object(|s) modified/, "not.exist");
agHelper.AssertContains(/[0-9] librar(y|ies) modified/, "not.exist");
// Commenting it as part of #28012 - to be added back later
// agHelper.AssertContains(/[0-9] librar(y|ies) modified/, "not.exist");
agHelper.GetNAssertElementText(
gitSync._gitStatusChanges,
"Some of the changes above are due to an improved file structure designed to reduce merge conflicts. You can safely commit them to your repository.",

View File

@ -10,14 +10,13 @@ describe("excludeForAirgap", "Tests JS Libraries", () => {
_.installer.assertUnInstall("uuidjs");
});
it("2. Checks for naming collision", () => {
it("2. Installs the library against a unique namespace when there is a collision with the existing entity", () => {
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.TABLE, 200, 200);
_.entityExplorer.NavigateToSwitcher("Explorer");
_.entityExplorer.RenameEntityFromExplorer("Table1", "jsonwebtoken");
_.entityExplorer.ExpandCollapseEntity("Libraries");
_.installer.OpenInstaller();
_.installer.installLibrary("jsonwebtoken", "jsonwebtoken", false);
_.agHelper.AssertContains("Name collision detected: jsonwebtoken");
_.installer.installLibrary("jsonwebtoken", "jsonwebtoken_1", true);
});
it("3. Checks jspdf library", () => {

View File

@ -1,64 +1,342 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1280,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 7,
"minHeight": 1292,
"parentColumnSpace": 1,
"leftColumn": 0,
"dynamicBindingPathList": [],
"children": [
"clientSchemaVersion": 1.0,
"serverSchemaVersion": 6.0,
"exportedApplication": {
"name": "xml_paser_test",
"isPublic": false,
"pages": [
{
"isVisible": true,
"text": "{{xmlParser.parse(Input1.text) }}",
"textStyle": "LABEL",
"textAlign": "LEFT",
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 3,
"rightColumn": 8,
"topRow": 3,
"bottomRow": 10,
"parentId": "0",
"widgetId": "axlcnmjk4t",
"dynamicBindingPathList": [
{
"key": "text"
}
]
},
{
"isVisible": true,
"inputType": "TEXT",
"label": "",
"widgetName": "Input1",
"type": "INPUT_WIDGET_V2",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 3,
"rightColumn": 8,
"topRow": 2,
"bottomRow": 3,
"parentId": "0",
"widgetId": "8n5urob9mz",
"dynamicBindingPathList": [],
"defaultText": "<note> <to>Tove</to> <from>Jani</from> <heading>Reminder</heading> <body>Don't forget me this weekend!</body> </note>"
"id": "Page1",
"isDefault": true
}
]
],
"publishedPages": [
{
"id": "Page1",
"isDefault": true
}
],
"viewMode": false,
"appIsExample": false,
"unreadCommentThreads": 0.0,
"color": "#EAEDFB",
"icon": "yen",
"slug": "xml-paser-test",
"unpublishedCustomJSLibs": [],
"publishedCustomJSLibs": [],
"evaluationVersion": 2.0,
"applicationVersion": 2.0,
"collapseInvisibleWidgets": true,
"isManualUpdate": false,
"deleted": false
},
"datasourceList": [],
"customJSLibList": [],
"pageList": [
{
"unpublishedPage": {
"name": "Page1",
"slug": "page1",
"layouts": [
{
"viewMode": false,
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 4896.0,
"snapColumns": 64.0,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0.0,
"bottomRow": 380.0,
"containerStyle": "none",
"snapRows": 124.0,
"parentRowSpace": 1.0,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 87.0,
"minHeight": 1292.0,
"dynamicTriggerPathList": [],
"parentColumnSpace": 1.0,
"dynamicBindingPathList": [],
"leftColumn": 0.0,
"children": [
{
"mobileBottomRow": 14.0,
"widgetName": "Text1",
"displayName": "Text",
"iconSVG": "https://release-appcdn.appsmith.com/static/media/icon.a47d6d5dbbb718c4dc4b2eb4f218c1b7.svg",
"searchTags": [
"typography",
"paragraph",
"label"
],
"topRow": 0.0,
"bottomRow": 16.0,
"parentRowSpace": 10.0,
"type": "TEXT_WIDGET",
"hideCard": false,
"mobileRightColumn": 23.0,
"animateLoading": true,
"overflow": "NONE",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"parentColumnSpace": 14.953125,
"dynamicTriggerPathList": [],
"leftColumn": 0.0,
"dynamicBindingPathList": [
{
"key": "truncateButtonColor"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
},
{
"key": "text"
}
],
"shouldTruncate": false,
"truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"text": "{{xmlParser.parse(Input1.text)}}",
"key": "bdg0tfrf6x",
"isDeprecated": false,
"rightColumn": 13.0,
"textAlign": "LEFT",
"dynamicHeight": "AUTO_HEIGHT",
"widgetId": "b8blvt9b2z",
"minWidth": 450.0,
"isVisible": true,
"fontStyle": "BOLD",
"textColor": "#231F20",
"version": 1.0,
"parentId": "0",
"tags": [
"Suggested",
"Content"
],
"renderMode": "CANVAS",
"isLoading": false,
"mobileTopRow": 10.0,
"responsiveBehavior": "fill",
"originalTopRow": 0.0,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"mobileLeftColumn": 7.0,
"maxDynamicHeight": 9000.0,
"originalBottomRow": 4.0,
"fontSize": "1rem",
"minDynamicHeight": 4.0
},
{
"boxShadow": "none",
"iconSVG": "https://release-appcdn.appsmith.com/static/media/icon.f2c34197dbcf03595098986de898928f.svg",
"topRow": 17.0,
"labelWidth": 5.0,
"type": "INPUT_WIDGET_V2",
"animateLoading": true,
"resetOnSubmit": true,
"leftColumn": 0.0,
"dynamicBindingPathList": [
{
"key": "accentColor"
},
{
"key": "borderRadius"
}
],
"labelStyle": "",
"inputType": "TEXT",
"isDisabled": false,
"isRequired": false,
"dynamicHeight": "FIXED",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"showStepArrows": false,
"isVisible": true,
"version": 2.0,
"tags": [
"Suggested",
"Inputs"
],
"isLoading": false,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"originalBottomRow": 21.0,
"mobileBottomRow": 21.0,
"widgetName": "Input1",
"displayName": "Input",
"searchTags": [
"form",
"text input",
"number",
"textarea"
],
"bottomRow": 24.0,
"parentRowSpace": 10.0,
"autoFocus": false,
"hideCard": false,
"mobileRightColumn": 19.0,
"parentColumnSpace": 14.34375,
"dynamicTriggerPathList": [],
"labelPosition": "Top",
"key": "mx0emg6xlv",
"labelTextSize": "0.875rem",
"isDeprecated": false,
"rightColumn": 19.0,
"widgetId": "ni16fc0xys",
"minWidth": 450.0,
"label": "Label",
"parentId": "0",
"labelAlignment": "left",
"renderMode": "CANVAS",
"mobileTopRow": 14.0,
"responsiveBehavior": "fill",
"originalTopRow": 14.0,
"mobileLeftColumn": 0.0,
"maxDynamicHeight": 9000.0,
"iconAlign": "left",
"defaultText": "<note> <to>Tove</to> <from>Jani</from> <heading>Reminder</heading> <body>Don't forget me this weekend!</body> </note>",
"minDynamicHeight": 4.0
},
{
"mobileBottomRow": 35.0,
"widgetName": "Text2",
"displayName": "Text",
"iconSVG": "https://release-appcdn.appsmith.com/static/media/icon.a47d6d5dbbb718c4dc4b2eb4f218c1b7.svg",
"searchTags": [
"typography",
"paragraph",
"label"
],
"topRow": 31.0,
"bottomRow": 35.0,
"parentRowSpace": 10.0,
"type": "TEXT_WIDGET",
"hideCard": false,
"mobileRightColumn": 16.0,
"animateLoading": true,
"overflow": "NONE",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"parentColumnSpace": 14.34375,
"leftColumn": 0.0,
"dynamicBindingPathList": [
{
"key": "truncateButtonColor"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
},
{
"key": "text"
}
],
"shouldTruncate": false,
"truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"text": "Hello {{appsmith.user.name || appsmith.user.email}}",
"key": "bdg0tfrf6x",
"isDeprecated": false,
"rightColumn": 20.0,
"textAlign": "LEFT",
"dynamicHeight": "AUTO_HEIGHT",
"widgetId": "3m7c3st35v",
"minWidth": 450.0,
"isVisible": true,
"fontStyle": "BOLD",
"textColor": "#231F20",
"version": 1.0,
"parentId": "0",
"tags": [
"Suggested",
"Content"
],
"renderMode": "CANVAS",
"isLoading": false,
"mobileTopRow": 31.0,
"responsiveBehavior": "fill",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"mobileLeftColumn": 0.0,
"maxDynamicHeight": 9000.0,
"fontSize": "1rem",
"minDynamicHeight": 4.0
}
]
},
"layoutOnLoadActions": [],
"layoutOnLoadActionErrors": [],
"validOnPageLoadActions": true,
"id": "Page1",
"deleted": false,
"policies": [],
"userPermissions": []
}
],
"userPermissions": [],
"policies": []
},
"publishedPage": {
"name": "Page1",
"slug": "page1",
"layouts": [
{
"viewMode": false,
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224.0,
"snapColumns": 16.0,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0.0,
"bottomRow": 1250.0,
"containerStyle": "none",
"snapRows": 33.0,
"parentRowSpace": 1.0,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 4.0,
"minHeight": 1292.0,
"dynamicTriggerPathList": [],
"parentColumnSpace": 1.0,
"dynamicBindingPathList": [],
"leftColumn": 0.0,
"children": []
},
"validOnPageLoadActions": true,
"id": "Page1",
"deleted": false,
"policies": [],
"userPermissions": []
}
],
"userPermissions": [],
"policies": []
},
"deleted": false,
"gitSyncId": "6529174c97a7581320fb6a4f_6529174c97a7581320fb6a51"
}
],
"actionList": [],
"actionCollectionList": [],
"updatedResources": {
"customJSLibList": [],
"actionList": [],
"pageList": [
"Page1"
],
"actionCollectionList": []
},
"editModeTheme": {
"name": "Default-New",
"displayName": "Modern",
"isSystemTheme": true,
"deleted": false
},
"publishedTheme": {
"name": "Default-New",
"displayName": "Modern",
"isSystemTheme": true,
"deleted": false
}
}
}

View File

@ -104,7 +104,6 @@
"echarts-gl": "^2.0.9",
"fast-deep-equal": "^3.1.3",
"fast-sort": "^3.4.0",
"fast-xml-parser": "^3.17.5",
"fastdom": "^1.0.11",
"focus-trap-react": "^8.9.2",
"fuse.js": "^3.4.5",

View File

@ -1,5 +1,5 @@
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
export function fetchJSLibraries(applicationId: string) {
return {
@ -8,7 +8,7 @@ export function fetchJSLibraries(applicationId: string) {
};
}
export function installLibraryInit(payload: Partial<TJSLibrary>) {
export function installLibraryInit(payload: Partial<JSLibrary>) {
return {
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,
payload,
@ -22,7 +22,7 @@ export function toggleInstaller(payload: boolean) {
};
}
export function uninstallLibraryInit(payload: TJSLibrary) {
export function uninstallLibraryInit(payload: JSLibrary) {
return {
type: ReduxActionTypes.UNINSTALL_LIBRARY_INIT,
payload,

View File

@ -1,5 +1,5 @@
import { APP_MODE } from "entities/App";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import Api from "./Api";
export default class LibraryApi extends Api {
@ -10,7 +10,7 @@ export default class LibraryApi extends Api {
static async addLibrary(
applicationId: string,
library: Partial<TJSLibrary> & { defs: string },
library: Partial<JSLibrary> & { defs: string },
) {
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/add";
return Api.patch(url, library);
@ -18,7 +18,7 @@ export default class LibraryApi extends Api {
static async removeLibrary(
applicationId: string,
library: Partial<TJSLibrary>,
library: Partial<JSLibrary>,
) {
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/remove";
return Api.patch(url, library);

View File

@ -39,7 +39,7 @@ import {
import { InstallState } from "reducers/uiReducers/libraryReducer";
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
import { getFormValues } from "redux-form";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
@ -1099,7 +1099,7 @@ export const selectLibrariesForExplorer = createSelector(
version: recommendedLibrary?.version || "",
url: recommendedLibrary?.url || url,
accessor: [],
} as TJSLibrary;
} as JSLibrary;
});
return [...queuedInstalls, ...libs];
},

View File

@ -1,39 +0,0 @@
{
"!name": "LIB/xmlParser",
"xmlParser": {
"parse": {
"!doc": "converts xml string to json object",
"!type": "fn(xml: string, options?: object, validationOption?: object) -> object"
},
"validate": {
"!doc": "validate xml data",
"!type": "fn(xml: string) -> bool"
},
"convertToJson": {
"!type": "fn(node: ?, options: object) -> ?"
},
"convertToJsonString": {
"!type": "fn(node: ?, options: object) -> string"
},
"convertTonimn": {
"!type": "fn(node: ?, e_schema: ?, options: object) -> ?"
},
"getTraversalObj": {
"!type": "fn(xmlData: ?, options: object) -> ?"
},
"j2xParser": {
"!type": "fn(options: object) -> object",
"prototype": {
"parse": {
"!type": "fn(jObj: ?)"
},
"j2x": {
"!type": "fn(jObj: ?, level: ?)"
}
}
},
"parseToNimn": {
"!type": "fn(xmlData: ?, schema: ?, options: ?) -> ?"
}
}
}

View File

@ -36,7 +36,7 @@ import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLib
import type { AppState } from "@appsmith/reducers";
import { installLibraryInit } from "actions/JSLibraryActions";
import classNames from "classnames";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { EntityClassNames } from "pages/Editor/Explorer/Entity";
@ -317,7 +317,7 @@ export function Installer() {
}, [URL, isValid]);
const installLibrary = useCallback(
(lib?: Partial<TJSLibrary>) => {
(lib?: Partial<JSLibrary>) => {
const url = lib?.url || URL;
const isQueued = queuedLibraries.find((libURL) => libURL === url);
if (isQueued) return;

View File

@ -33,7 +33,7 @@ import {
uninstallLibraryInit,
} from "actions/JSLibraryActions";
import EntityAddButton from "../Entity/AddButton";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import {
getCurrentPageId,
getPagePermissions,
@ -177,7 +177,7 @@ const Version = styled.div<{ version?: string }>`
margin: ${(props) => (props.version ? "0 8px" : "0")};
`;
const PrimaryCTA = function ({ lib }: { lib: TJSLibrary }) {
const PrimaryCTA = function ({ lib }: { lib: JSLibrary }) {
const installationStatus = useSelector(selectInstallationStatus);
const dispatch = useDispatch();
@ -215,7 +215,7 @@ const PrimaryCTA = function ({ lib }: { lib: TJSLibrary }) {
return null;
};
function LibraryEntity({ lib }: { lib: TJSLibrary }) {
function LibraryEntity({ lib }: { lib: JSLibrary }) {
const openDocs = useCallback(
(url?: string) => (e: React.MouseEvent) => {
e?.stopPropagation();

View File

@ -5,9 +5,19 @@ export default [
author: "auth0",
docsURL: "https://github.com/auth0/node-jsonwebtoken#readme",
version: "8.5.1",
url: `/libraries/jsonwebtoken@8.5.1.js`,
url: "/libraries/jsonwebtoken@8.5.1.js",
icon: "https://github.com/auth0.png?s=20",
},
{
name: "fast-xml-parser",
description:
"Validate XML, Parse XML to JS Object, or Build XML from JS Object without C/C++ based libraries and no callback.",
author: "NaturalIntelligence",
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
url: "https://cdnjs.cloudflare.com/ajax/libs/fast-xml-parser/4.3.2/fxparser.min.js",
version: "4.3.2",
icon: "https://img.jsdelivr.com/github.com/NaturalIntelligence.png",
},
{
name: "jspdf",
description: "PDF Document creation from JavaScript",

View File

@ -10,7 +10,7 @@ import type {
} from "workers/Evaluation/evaluate";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
export enum LINT_WORKER_ACTIONS {
LINT_TREE = "LINT_TREE",
@ -78,5 +78,5 @@ export interface getLintErrorsFromTreeResponse {
export interface updateJSLibraryProps {
add?: boolean;
libs: TJSLibrary[];
libs: JSLibrary[];
}

View File

@ -5,7 +5,7 @@ import {
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import { defaultLibraries } from "workers/common/JSLibrary";
export enum InstallState {
@ -17,14 +17,14 @@ export enum InstallState {
export interface LibraryState {
installationStatus: Record<string, InstallState>;
installedLibraries: TJSLibrary[];
installedLibraries: JSLibrary[];
isInstallerOpen: boolean;
}
const initialState = {
isInstallerOpen: false,
installationStatus: {},
installedLibraries: defaultLibraries.map((lib: TJSLibrary) => {
installedLibraries: defaultLibraries.map((lib: JSLibrary) => {
return {
name: lib.name,
docsURL: lib.docsURL,
@ -38,7 +38,7 @@ const initialState = {
const jsLibraryReducer = createImmerReducer(initialState, {
[ReduxActionTypes.INSTALL_LIBRARY_INIT]: (
state: LibraryState,
action: ReduxAction<Partial<TJSLibrary>>,
action: ReduxAction<Partial<JSLibrary>>,
) => {
const { url } = action.payload;
state.installationStatus[url as string] =
@ -91,7 +91,7 @@ const jsLibraryReducer = createImmerReducer(initialState, {
},
[ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS]: (
state: LibraryState,
action: ReduxAction<TJSLibrary[]>,
action: ReduxAction<JSLibrary[]>,
) => {
state.installedLibraries = action.payload.concat(
initialState.installedLibraries,
@ -99,7 +99,7 @@ const jsLibraryReducer = createImmerReducer(initialState, {
},
[ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS]: (
state: LibraryState,
action: ReduxAction<TJSLibrary>,
action: ReduxAction<JSLibrary>,
) => {
const uLib = action.payload;
state.installedLibraries = state.installedLibraries.filter(

View File

@ -29,7 +29,7 @@ import log from "loglevel";
import { APP_MODE } from "entities/App";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import { getUsedActionNames } from "selectors/actionSelectors";
import AppsmithConsole from "utils/AppsmithConsole";
import { selectInstalledLibraries } from "@appsmith/selectors/entitiesSelector";
@ -73,7 +73,7 @@ function* handleInstallationFailure(
log.error(message);
}
export function* installLibrarySaga(lib: Partial<TJSLibrary>) {
export function* installLibrarySaga(lib: Partial<JSLibrary>) {
const { url } = lib;
const takenNamesMap: Record<string, true> = yield select(
@ -81,7 +81,7 @@ export function* installLibrarySaga(lib: Partial<TJSLibrary>) {
"",
);
const installedLibraries: TJSLibrary[] = yield select(
const installedLibraries: JSLibrary[] = yield select(
selectInstalledLibraries,
);
@ -232,7 +232,7 @@ export function* installLibrarySaga(lib: Partial<TJSLibrary>) {
});
}
function* uninstallLibrarySaga(action: ReduxAction<TJSLibrary>) {
function* uninstallLibrarySaga(action: ReduxAction<JSLibrary>) {
const { accessor, name } = action.payload;
const applicationId: string = yield select(getCurrentApplicationId);
@ -320,7 +320,7 @@ function* fetchJSLibraries(action: ReduxAction<string>) {
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) return;
const libraries = response.data as Array<TJSLibrary & { defs: string }>;
const libraries = response.data as Array<JSLibrary & { defs: string }>;
const { message, success }: { success: boolean; message: string } =
yield call(
@ -404,7 +404,7 @@ function* startInstallationRequestChannel() {
ReduxActionTypes.INSTALL_LIBRARY_INIT,
]);
while (true) {
const action: ReduxAction<Partial<TJSLibrary>> =
const action: ReduxAction<Partial<JSLibrary>> =
yield take(queueInstallChannel);
yield put({
type: ReduxActionTypes.INSTALL_LIBRARY_START,

View File

@ -4,7 +4,7 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { APP_MODE } from "entities/App";
import { call, put, select, takeEvery } from "redux-saga/effects";
import { getAppMode } from "@appsmith/selectors/entitiesSelector";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import { logLatestLintPropertyErrors } from "./PostLintingSagas";
import { getAppsmithConfigs } from "@appsmith/configs";
import type { AppState } from "@appsmith/reducers";
@ -29,7 +29,7 @@ const APPSMITH_CONFIGS = getAppsmithConfigs();
export const lintWorker = new Linter();
function* updateLintGlobals(
action: ReduxAction<{ add?: boolean; libs: TJSLibrary[] }>,
action: ReduxAction<{ add?: boolean; libs: JSLibrary[] }>,
) {
const appMode: APP_MODE = yield select(getAppMode);
const isEditorMode = appMode === APP_MODE.EDIT;

View File

@ -2,7 +2,7 @@ import type { DataTree } from "entities/DataTree/dataTreeTypes";
import { createSelector } from "reselect";
import WidgetFactory from "WidgetProvider/factory";
import type { FlattenedWidgetProps } from "WidgetProvider/constants";
import type { TJSLibrary } from "workers/common/JSLibrary";
import type { JSLibrary } from "workers/common/JSLibrary";
import { getDataTree } from "./dataTreeSelectors";
import {
getExistingPageNames,
@ -28,7 +28,7 @@ export const getUsedActionNames = createSelector(
pageNames: Record<string, any>,
dataTree: DataTree,
parentWidget: FlattenedWidgetProps | undefined,
installedLibraries: TJSLibrary[],
installedLibraries: JSLibrary[],
) => {
const map: Record<string, boolean> = {};
// The logic has been copied from Explorer/Entity/Name.tsx Component.

View File

@ -7,14 +7,14 @@ import * as mod from "../../../common/JSLibrary/ternDefinitionGenerator";
jest.mock("../../../common/JSLibrary/ternDefinitionGenerator");
declare const self: WorkerGlobalScope;
describe("Tests to assert install/uninstall flows", function () {
beforeAll(() => {
self.importScripts = jest.fn(() => {
//@ts-expect-error importScripts is not defined in the test environment
self.lodash = {};
});
//@ts-expect-error importScripts is not defined in the test environment
self.import = jest.fn();
const mockTernDefsGenerator = jest.fn(() => ({}));
@ -49,7 +49,7 @@ describe("Tests to assert install/uninstall flows", function () {
});
});
it("Reinstalling a different version of the same installed library should fail", async function () {
it("Reinstalling a different version of the same installed library should create a new accessor", async function () {
const res = await installLibrary({
data: {
url: "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
@ -59,39 +59,43 @@ describe("Tests to assert install/uninstall flows", function () {
method: EVAL_WORKER_ASYNC_ACTION.INSTALL_LIBRARY,
});
expect(res).toEqual({
success: false,
defs: {},
error: expect.any(Error),
success: true,
defs: {
"!name": "LIB/lodash_1",
lodash_1: undefined,
},
accessor: ["lodash_1"],
});
});
it("Detects name space collision where there is another entity(api, widget or query) with the same name", async function () {
//@ts-expect-error ignore
delete self.lodash;
it("Detects name space collision where there is another entity(api, widget or query) with the same name and creates a unique accessor", async function () {
delete self["lodash"];
const res = await installLibrary({
data: {
url: "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
takenAccessors: [],
takenAccessors: ["lodash_1"],
takenNamesMap: { lodash: true },
},
method: EVAL_WORKER_ASYNC_ACTION.INSTALL_LIBRARY,
});
expect(res).toEqual({
success: false,
defs: {},
error: expect.any(Error),
success: true,
defs: {
"!name": "LIB/lodash_2",
lodash_2: undefined,
},
accessor: ["lodash_2"],
});
delete self["lodash_2"];
});
it("Removes or set the accessors to undefined on the global object on uninstallation", async function () {
//@ts-expect-error ignore
it("Removes or set the accessors to undefined on the global object on un-installation", async function () {
self.lodash = {};
const res = await uninstallLibrary({
data: ["lodash"],
method: EVAL_WORKER_SYNC_ACTION.UNINSTALL_LIBRARY,
});
expect(res).toEqual({ success: true });
//@ts-expect-error ignore
expect(self.lodash).toBeUndefined();
});
});

View File

@ -9,10 +9,20 @@ import {
JSLibraries,
libraryReservedIdentifiers,
} from "../../common/JSLibrary";
import type { JSLibrary } from "../../common/JSLibrary";
import { resetJSLibraries } from "../../common/JSLibrary/resetJSLibraries";
import { makeTernDefs } from "../../common/JSLibrary/ternDefinitionGenerator";
import type { EvalWorkerASyncRequest, EvalWorkerSyncRequest } from "../types";
import { dataTreeEvaluator } from "./evalTree";
import log from "loglevel";
declare global {
interface WorkerGlobalScope {
[x: string]: any;
}
}
declare const self: WorkerGlobalScope;
enum LibraryInstallError {
NameCollisionError,
@ -21,16 +31,6 @@ enum LibraryInstallError {
LibraryOverrideError,
}
class NameCollisionError extends Error {
code = LibraryInstallError.NameCollisionError;
constructor(accessors: string) {
super(
createMessage(customJSLibraryMessages.NAME_COLLISION_ERROR, accessors),
);
this.name = "NameCollisionError";
}
}
class ImportError extends Error {
code = LibraryInstallError.ImportError;
constructor(url: string) {
@ -47,25 +47,13 @@ class TernDefinitionError extends Error {
}
}
class LibraryOverrideError extends Error {
code = LibraryInstallError.LibraryOverrideError;
data: any;
constructor(name: string, data: any) {
super(createMessage(customJSLibraryMessages.LIB_OVERRIDE_ERROR, name));
this.name = "LibraryOverrideError";
this.data = data;
}
}
const removeDataTreeFromContext = () => {
if (!dataTreeEvaluator) return {};
const evalTree = dataTreeEvaluator?.getEvalTree();
const dataTreeEntityNames = Object.keys(evalTree);
const tempDataTreeStore: Record<string, any> = {};
for (const entityName of dataTreeEntityNames) {
// @ts-expect-error: self is a global variable
tempDataTreeStore[entityName] = self[entityName];
// @ts-expect-error: self is a global variable
delete self[entityName];
}
return tempDataTreeStore;
@ -76,12 +64,17 @@ function addTempStoredDataTreeToContext(
) {
const dataTreeEntityNames = Object.keys(tempDataTreeStore);
for (const entityName of dataTreeEntityNames) {
// @ts-expect-error: self is a global variable
self[entityName] = tempDataTreeStore[entityName];
}
}
export async function installLibrary(request: EvalWorkerASyncRequest) {
export async function installLibrary(
request: EvalWorkerASyncRequest<{
url: string;
takenNamesMap: Record<string, true>;
takenAccessors: Array<string>;
}>,
) {
const { data } = request;
const { takenAccessors, takenNamesMap, url } = data;
const defs: Def = {};
@ -91,73 +84,105 @@ export async function installLibrary(request: EvalWorkerASyncRequest) {
* We store the data tree in a temporary variable and add it back to the global scope after the library is imported.
*/
const tempDataTreeStore = removeDataTreeFromContext();
// Map of all the currently installed libraries
const libStore = takenAccessors.reduce(
(acc: Record<string, unknown>, a: string) => {
acc[a] = self[a];
return acc;
},
{},
);
try {
const currentEnvKeys = Object.keys(self);
const envKeysBeforeInstallation = Object.keys(self);
//@ts-expect-error Find libraries that were uninstalled.
const unsetKeys = currentEnvKeys.filter((key) => self[key] === undefined);
/** Holds keys of uninstalled libraries that cannot be removed from worker global.
* Certain libraries are added to the global scope with { configurable: false }
*/
const unsetLibraryKeys = envKeysBeforeInstallation.filter(
(k) => self[k] === undefined,
);
const accessors: string[] = [];
const existingLibraries: Record<string, any> = {};
for (const acc of takenAccessors) {
existingLibraries[acc] = self[acc];
}
let module = null;
try {
self.importScripts(url);
// Find keys add that were installed to the global scope.
const keysAfterInstallation = Object.keys(self);
accessors.push(
...difference(keysAfterInstallation, envKeysBeforeInstallation),
);
// Check the list of installed library to see if their values have changed.
// This is to check if the newly installed library overwrites an already existing
accessors.push(
...Object.keys(libStore).filter((k) => {
return libStore[k] !== self[k];
}),
);
accessors.push(...unsetLibraryKeys.filter((k) => self[k] !== undefined));
for (let i = 0; i < accessors.length; i++) {
if (
takenNamesMap.hasOwnProperty(accessors[i]) ||
takenAccessors.includes(accessors[i])
) {
const uniqueName = generateUniqueAccessor(
accessors[i],
takenAccessors,
takenNamesMap,
);
self[uniqueName] = self[accessors[i]];
accessors[i] = uniqueName;
}
}
} catch (e) {
log.debug(e, `importScripts failed for ${url}`);
try {
module = await import(/* webpackIgnore: true */ url);
if (module && typeof module === "object") {
const uniqAccessor = generateUniqueAccessor(
url,
takenAccessors,
takenNamesMap,
);
self[uniqAccessor] = flattenModule(module);
accessors.push(uniqAccessor);
}
} catch (e) {
log.debug(e, `dynamic import failed for ${url}`);
throw new ImportError(url);
}
}
// Find keys add that were installed to the global scope.
const accessors = difference(
Object.keys(self),
currentEnvKeys,
) as Array<string>;
// If no keys were added to the global scope, check if the module is a ESM module.
// If no accessors at this point, installation likely failed.
if (accessors.length === 0) {
if (module && typeof module === "object") {
const uniqAccessor = generateUniqueAccessor(
url,
takenAccessors,
takenNamesMap,
);
// @ts-expect-error no types
self[uniqAccessor] = flattenModule(module);
accessors.push(uniqAccessor);
}
throw new Error("Unable to determine a unique accessor");
}
addTempStoredDataTreeToContext(tempDataTreeStore);
checkForNameCollision(accessors, takenNamesMap);
checkIfUninstalledEarlier(accessors, unsetKeys);
checkForOverrides(url, accessors, takenAccessors, existingLibraries);
if (accessors.length === 0)
return { status: false, defs, accessor: accessors };
//Reserves accessor names.
const name = accessors[accessors.length - 1];
defs["!name"] = `LIB/${name}`;
try {
for (const key of accessors) {
//@ts-expect-error no types
defs[key] = makeTernDefs(self[key]);
}
} catch (e) {
for (const acc of accessors) {
//@ts-expect-error no types
self[acc] = undefined;
}
log.debug(e, `ternDefinitions failed for ${url}`);
throw new TernDefinitionError(
`Failed to generate autocomplete definitions: ${name}`,
);
}
// All the existing library and the newly installed one is added to global.
Object.keys(libStore).forEach((k) => (self[k] = libStore[k]));
//Reserve accessor names.
for (const acc of accessors) {
//we have to update invalidEntityIdentifiers as well
@ -167,21 +192,20 @@ export async function installLibrary(request: EvalWorkerASyncRequest) {
return { success: true, defs, accessor: accessors };
} catch (error) {
addTempStoredDataTreeToContext(tempDataTreeStore);
takenAccessors.forEach((k) => (self[k] = libStore[k]));
return { success: false, defs, error };
}
}
export function uninstallLibrary(request: EvalWorkerSyncRequest) {
export function uninstallLibrary(
request: EvalWorkerSyncRequest<Array<string>>,
) {
const { data } = request;
const accessor = data;
try {
for (const key of accessor) {
try {
delete self[key];
} catch (e) {
//@ts-expect-error ignore
self[key] = undefined;
}
self[key] = undefined;
//we have to update invalidEntityIdentifiers as well
delete libraryReservedIdentifiers[key];
delete invalidEntityIdentifiers[key];
@ -192,39 +216,60 @@ export function uninstallLibrary(request: EvalWorkerSyncRequest) {
}
}
export async function loadLibraries(request: EvalWorkerASyncRequest) {
export async function loadLibraries(
request: EvalWorkerASyncRequest<JSLibrary[]>,
) {
resetJSLibraries();
//Add types
const { data: libs } = request;
let message = "";
const libStore: Record<string, unknown> = {};
try {
for (const lib of libs) {
const url = lib.url;
const accessor = lib.accessor;
const url = lib.url as string;
const accessors = lib.accessor;
const keysBefore = Object.keys(self);
let module = null;
try {
self.importScripts(url);
const keysAfter = Object.keys(self);
const defaultAccessors = difference(keysAfter, keysBefore);
/**
* Installing 2 different version of lodash tries to add the same accessor on the self object. Let take version a & b for example.
* Installation of version a, will add _ to the self object and can be detected by looking at the differences in the previous step.
* Now when version b is installed, differences will be [], since _ already exists in the self object.
* We add all the installations to the libStore and see if the reference it points to in the self object changes.
* If the references changes it means that it a valid accessor.
*/
defaultAccessors.push(
...Object.keys(libStore).filter((k) => libStore[k] !== self[k]),
);
/** Sort the accessor list from backend and installed accessor list using the same rule to apply all modifications.
* This is required only for UMD builds, since we always generate unique names for ESM.
*/
accessors.sort();
defaultAccessors.sort();
for (let i = 0; i < defaultAccessors.length; i++) {
self[accessors[i]] = self[defaultAccessors[i]];
libStore[defaultAccessors[i]] = self[defaultAccessors[i]];
libraryReservedIdentifiers[accessors[i]] = true;
invalidEntityIdentifiers[accessors[i]] = true;
}
} catch (e) {
message = (e as Error).message;
try {
module = await import(/* webpackIgnore: true */ url);
if (!module || typeof module !== "object") throw "Not an ESM module";
const key = accessors[0];
libStore[key] = module;
libraryReservedIdentifiers[key] = true;
invalidEntityIdentifiers[key] = true;
} catch (e) {
message = (e as Error).message;
throw new ImportError(url);
}
}
const keysAfter = Object.keys(self);
const newKeys = difference(keysAfter, keysBefore);
if (newKeys.length === 0 && module && typeof module === "object") {
self[accessor[0]] = flattenModule(module);
newKeys.push(accessor[0]);
}
for (const key of newKeys) {
//we have to update invalidEntityIdentifiers as well
libraryReservedIdentifiers[key] = true;
invalidEntityIdentifiers[key] = true;
}
}
JSLibraries.push(...libs);
return { success: true, message };
@ -234,48 +279,6 @@ export async function loadLibraries(request: EvalWorkerASyncRequest) {
}
}
function checkForNameCollision(
accessor: string[],
takenNamesMap: Record<string, any>,
) {
const collidingNames = accessor.filter((key: string) => takenNamesMap[key]);
if (collidingNames.length) {
for (const acc of accessor) {
//@ts-expect-error no types
self[acc] = undefined;
}
throw new NameCollisionError(collidingNames.join(", "));
}
}
function checkIfUninstalledEarlier(accessor: string[], unsetKeys: string[]) {
if (accessor.length > 0) return;
for (const key of unsetKeys) {
//@ts-expect-error no types
if (!self[key]) continue;
accessor.push(key);
}
}
function checkForOverrides(
url: string,
accessor: string[],
takenAccessors: string[],
existingLibraries: Record<string, any>,
) {
if (accessor.length > 0) return;
const overriddenAccessors: Array<string> = [];
for (const acc of takenAccessors) {
//@ts-expect-error no types
if (existingLibraries[acc] === self[acc]) continue;
//@ts-expect-error no types
self[acc] = existingLibraries[acc];
overriddenAccessors.push(acc);
}
if (overriddenAccessors.length === 0) return;
throw new LibraryOverrideError(url, overriddenAccessors);
}
/**
* This function is called only for ESM modules and generates a unique namespace for the module.
* @param url
@ -284,22 +287,24 @@ function checkForOverrides(
* @returns
*/
function generateUniqueAccessor(
url: string,
urlOrName: string,
takenAccessors: Array<string>,
takenNamesMap: Record<string, true>,
) {
let name = urlOrName;
// extract file name from url
const urlObject = new URL(url);
// URL pattern for ESM modules from jsDelivr - https://cdn.jsdelivr.net/npm/stripe@13.3.0/+esm
// Assuming the file name is the last part of the path
const urlPathParts = urlObject.pathname.split("/");
let fileName = urlPathParts.pop();
fileName = fileName?.includes("esm") ? urlPathParts.pop() : fileName;
try {
// Checks to see if a URL was passed
const urlObject = new URL(urlOrName);
// URL pattern for ESM modules from jsDelivr - https://cdn.jsdelivr.net/npm/stripe@13.3.0/+esm
// Assuming the file name is the last part of the path
const urlPathParts = urlObject.pathname.split("/");
name = urlPathParts.pop() as string;
name = name?.includes("+esm") ? (urlPathParts.pop() as string) : name;
} catch (e) {}
// This should never happen. This is just to avoid the typescript error.
if (!fileName) throw new Error("Unable to generate a unique accessor");
// Replace all non-alphabetic characters with underscores and remove trailing underscores
const validVar = fileName.replace(/[^a-zA-Z]/g, "_").replace(/_+$/, "");
const validVar = name.replace(/[^a-zA-Z]/g, "_").replace(/_+$/, "");
if (
!takenAccessors.includes(validVar) &&
!takenNamesMap.hasOwnProperty(validVar)
@ -308,7 +313,7 @@ function generateUniqueAccessor(
}
let index = 0;
while (index++ < 100) {
const name = `Library_${index}`;
const name = `${validVar}_${index}`;
if (!takenAccessors.includes(name) && !takenNamesMap.hasOwnProperty(name)) {
return name;
}

View File

@ -19,9 +19,12 @@ import type { WorkerRequest } from "@appsmith/workers/common/types";
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { APP_MODE } from "entities/App";
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
export type EvalWorkerASyncRequest = WorkerRequest<
any,
export type EvalWorkerSyncRequest<T = any> = WorkerRequest<
T,
EVAL_WORKER_SYNC_ACTION
>;
export type EvalWorkerASyncRequest<T = any> = WorkerRequest<
T,
EVAL_WORKER_ASYNC_ACTION
>;
export type EvalWorkerResponse = EvalTreeResponseData | boolean | unknown;

View File

@ -6,7 +6,6 @@ import ecma from "constants/defs/ecmascript.json";
import lodash from "constants/defs/lodash.json";
import base64 from "constants/defs/base64-js.json";
import moment from "constants/defs/moment.json";
import xmlJs from "constants/defs/xmlParser.json";
import forge from "constants/defs/forge.json";
import browser from "constants/defs/browser.json";
import {
@ -64,7 +63,6 @@ function startServer(plugins = {}, scripts?: string[]) {
lodash,
base64,
moment,
xmlJs,
forge,
] as Def[],
plugins: plugins,

View File

@ -1,7 +1,7 @@
import lodashPackageJson from "lodash/package.json";
import momentPackageJson from "moment-timezone/package.json";
export interface TJSLibrary {
export interface JSLibrary {
version?: string;
docsURL: string;
name: string;
@ -9,7 +9,7 @@ export interface TJSLibrary {
url?: string;
}
export const defaultLibraries: TJSLibrary[] = [
export const defaultLibraries: JSLibrary[] = [
{
accessor: ["_"],
version: lodashPackageJson.version,
@ -22,12 +22,6 @@ export const defaultLibraries: TJSLibrary[] = [
docsURL: `https://momentjs.com/docs/`,
name: "moment",
},
{
accessor: ["xmlParser"],
version: "3.17.5",
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
name: "xmlParser",
},
{
accessor: ["forge"],
version: "1.3.0",

View File

@ -1,6 +1,5 @@
import _ from "./lodash-wrapper";
import moment from "moment-timezone";
import parser from "fast-xml-parser";
import forge from "node-forge";
import { defaultLibraries } from "./index";
import { JSLibraries, libraryReservedIdentifiers } from "./index";
@ -8,7 +7,6 @@ import { invalidEntityIdentifiers } from "../DependencyMap/utils";
const defaultLibImplementations = {
lodash: _,
moment: moment,
xmlParser: parser,
// We are removing some functionalities of node-forge because they wont
// work in the worker thread
forge: /*#__PURE*/ _.omit(forge, ["tls", "http", "xhr", "socket", "task"]),

View File

@ -10511,7 +10511,6 @@ __metadata:
factory.ts: ^0.5.1
fast-deep-equal: ^3.1.3
fast-sort: ^3.4.0
fast-xml-parser: ^3.17.5
fastdom: ^1.0.11
focus-trap-react: ^8.9.2
fuse.js: ^3.4.5
@ -16679,15 +16678,6 @@ __metadata:
languageName: node
linkType: hard
"fast-xml-parser@npm:^3.17.5":
version: 3.17.5
resolution: "fast-xml-parser@npm:3.17.5"
bin:
xml2js: cli.js
checksum: 6c436f540a4dcab67d395c0f6e8dc70090e6ced2ff73ed5fec181c1b25df755ed9fd9ba8271d6f70096fbe3add8b4eac130a5f4daeb12970427f72403a56934e
languageName: node
linkType: hard
"fastdom@npm:^1.0.11":
version: 1.0.11
resolution: "fastdom@npm:1.0.11"

View File

@ -1,8 +1,34 @@
package com.appsmith.server.constants;
import com.appsmith.server.domains.CustomJSLib;
import java.util.Set;
public class ApplicationConstants {
public static final String[] APP_CARD_COLORS = {
"#FFDEDE", "#FFEFDB", "#F3F1C7", "#F4FFDE", "#C7F3F0", "#D9E7FF", "#E3DEFF", "#F1DEFF", "#C7F3E3", "#F5D1D1",
"#ECECEC", "#FBF4ED", "#D6D1F2", "#FFEBFB", "#EAEDFB"
};
public static CustomJSLib getDefaultParserCustomJsLibCompatibilityDTO() {
CustomJSLib customJSLib = new CustomJSLib();
customJSLib.setName("xmlParser");
customJSLib.setVersion("3.17.5");
customJSLib.setAccessor(Set.of("xmlParser"));
customJSLib.setUrl("https://cdnjs.cloudflare.com/ajax/libs/fast-xml-parser/3.17.5/parser.min.js");
customJSLib.setDefs(
"{\"!name\":\"LIB/xmlParser\",\"xmlParser\":{\"parse\":{\"!type\":\"fn()\",\"prototype\":{}},\"convertTonimn\":{\"!type\":\"fn()\",\"prototype\":{}},\"getTraversalObj\":{\"!type\":\"fn()\",\"prototype\":{}},\"convertToJson\":{\"!type\":\"fn()\",\"prototype\":{}},\"convertToJsonString\":{\"!type\":\"fn()\",\"prototype\":{}},\"validate\":{\"!type\":\"fn()\",\"prototype\":{}},\"j2xParser\":{\"!type\":\"fn()\",\"prototype\":{\"parse\":{\"!type\":\"fn()\",\"prototype\":{}},\"j2x\":{\"!type\":\"fn()\",\"prototype\":{}}}},\"parseToNimn\":{\"!type\":\"fn()\",\"prototype\":{}}}}");
customJSLib.setUidString(XML_PARSER_LIBRARY_UID);
return customJSLib;
}
/**
* Appsmith provides xmlParser v 3.17.5 and few other customJSLibraries by default, xmlParser has been
* flagged because it has some vulnerabilities. Appsmith is stopping natively providing support for xmlParser.
* This however, would break existing applications which are using xmlParser. In order to prevent this,
* we are adding xmlParser as an add-onn to all existing applications and applications which will be imported
* This CustomJSLib UID needs to be added to all the imported applications where we don't have any later versions of xmlParser present.
*/
public static final String XML_PARSER_LIBRARY_UID =
"xmlParser_https://cdnjs.cloudflare.com/ajax/libs/fast-xml-parser/3.17.5/parser.min.js";
}

View File

@ -16,6 +16,7 @@ import com.appsmith.external.models.DefaultResources;
import com.appsmith.external.models.OAuth2;
import com.appsmith.external.models.Policy;
import com.appsmith.server.actioncollections.base.ActionCollectionService;
import com.appsmith.server.constants.ApplicationConstants;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.ResourceModes;
import com.appsmith.server.datasources.base.DatasourceService;
@ -1135,6 +1136,8 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
customJSLibs = new ArrayList<>();
}
ensureXmlParserPresenceInCustomJsLibList(customJSLibs);
return Flux.fromIterable(customJSLibs)
.flatMap(customJSLib -> {
customJSLib.setId(null);
@ -1154,6 +1157,32 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
});
}
/**
* This method takes customJSLibList from application JSON, checks if an entry for XML parser exists,
* otherwise adds the entry.
* This has been done to add the xmlParser entry in imported application as appsmith is stopping native support
* for xml parser.
* Read More: https://github.com/appsmithorg/appsmith/pull/28012
*
* @param customJSLibList
*/
public void ensureXmlParserPresenceInCustomJsLibList(List<CustomJSLib> customJSLibList) {
boolean isXmlParserLibFound = false;
for (CustomJSLib customJSLib : customJSLibList) {
if (!customJSLib.getUidString().equals(ApplicationConstants.XML_PARSER_LIBRARY_UID)) {
continue;
}
isXmlParserLibFound = true;
break;
}
if (!isXmlParserLibFound) {
CustomJSLib xmlParserJsLib = ApplicationConstants.getDefaultParserCustomJsLibCompatibilityDTO();
customJSLibList.add(xmlParserJsLib);
}
}
private Mono<List<Datasource>> getExistingDatasourceMono(String applicationId, Flux<Datasource> datasourceFlux) {
Mono<List<Datasource>> existingDatasourceMono;
// Check if the request is to hydrate the application to DB for particular branch

View File

@ -3,6 +3,7 @@ package com.appsmith.server.migrations;
import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.InvisibleActionFields;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.ResourceModes;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.NewAction;
@ -236,4 +237,23 @@ public class MigrationHelperMethods {
mongoTemplate.find(query(where(fieldName(path)).is(id)), type);
return domainObject;
}
/**
* The method provides the criteria for any document to qualify as not deleted
* @return Criteria
*/
public static Criteria notDeleted() {
return new Criteria()
.andOperator(
// Older check for deleted
new Criteria()
.orOperator(
where(FieldName.DELETED).exists(false),
where(FieldName.DELETED).is(false)),
// New check for deleted
new Criteria()
.orOperator(
where(FieldName.DELETED_AT).exists(false),
where(FieldName.DELETED_AT).is(null)));
}
}

View File

@ -0,0 +1,90 @@
package com.appsmith.server.migrations.db.ce;
import com.appsmith.server.constants.ApplicationConstants;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.QApplication;
import com.appsmith.server.dtos.CustomJSLibApplicationDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import static com.appsmith.server.constants.ApplicationConstants.XML_PARSER_LIBRARY_UID;
import static com.appsmith.server.migrations.MigrationHelperMethods.notDeleted;
import static com.appsmith.server.repositories.ce.BaseAppsmithRepositoryCEImpl.fieldName;
/**
* Appsmith provides xmlParser v 3.17.5 and few other customJSLibraries by default, xmlParser has been
* flagged because it has some vulnerabilities. Appsmith is stopping natively providing support for xmlParser.
* This however, would break existing applications which are using xmlParser. In order to prevent this,
* applications require to have xmlParser as added library.
*
* This migration takes care of adding a document in customJsLib for xml-parser and adding corresponding entry
* in application collection
*
*/
@Slf4j
@ChangeUnit(order = "032", id = "add-xml-parser-to-application-jslibs", author = " ")
public class Migration032AddingXmlParserToApplicationLibraries {
private final MongoTemplate mongoTemplate;
private static final String UNPUBLISHED_CUSTOM_JS_LIBS =
fieldName(QApplication.application.unpublishedCustomJSLibs);
private static final String PUBLISHED_CUSTOM_JS_LIBS = fieldName(QApplication.application.publishedCustomJSLibs);
public Migration032AddingXmlParserToApplicationLibraries(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@RollbackExecution
public void rollbackExecution() {}
@Execution
public void addXmlParserEntryToEachApplication() {
// add the xml-parser to customJSLib if it's not already present
CustomJSLib customJSLib = generateXmlParserJSLibObject();
try {
mongoTemplate.save(customJSLib);
} catch (DuplicateKeyException duplicateKeyException) {
log.debug(
"Addition of xmlParser object in customJSLib failed, because object with similar UID already exists");
} catch (Exception exception) {
throw new AppsmithException(
AppsmithError.MIGRATION_FAILED,
"Migration032AddingXmlParserToApplicationLibraries",
exception.getMessage(),
"Unable to insert xml parser library in CustomJSLib collection");
}
// add uid entry in all these custom js libs
Update pushXmlParserUpdate = new Update()
.addToSet(PUBLISHED_CUSTOM_JS_LIBS, getXmlParserCustomJSLibApplicationDTO())
.addToSet(UNPUBLISHED_CUSTOM_JS_LIBS, getXmlParserCustomJSLibApplicationDTO());
log.debug("Going to add Xml Parser uid in all application DTOs");
mongoTemplate.updateMulti(
new Query().addCriteria(getMigrationCriteria()), pushXmlParserUpdate, Application.class);
}
private CustomJSLibApplicationDTO getXmlParserCustomJSLibApplicationDTO() {
CustomJSLibApplicationDTO xmlParserApplicationDTO = new CustomJSLibApplicationDTO();
xmlParserApplicationDTO.setUidString(XML_PARSER_LIBRARY_UID);
return xmlParserApplicationDTO;
}
private static CustomJSLib generateXmlParserJSLibObject() {
return ApplicationConstants.getDefaultParserCustomJsLibCompatibilityDTO();
}
private static Criteria getMigrationCriteria() {
return notDeleted();
}
}

View File

@ -968,7 +968,10 @@ public class ImportApplicationServiceTests {
final List<ActionCollection> actionCollectionList = tuple.getT5();
final List<CustomJSLib> importedJSLibList = tuple.getT6();
assertEquals(1, importedJSLibList.size());
// although the imported list had only one jsLib entry, the other entry comes from ensuring an xml
// parser entry
// for backward compatibility
assertEquals(2, importedJSLibList.size());
CustomJSLib importedJSLib = (CustomJSLib) importedJSLibList.toArray()[0];
CustomJSLib expectedJSLib = new CustomJSLib(
"TestLib", Set.of("accessor1"), "url", "docsUrl", "1" + ".0", "defs_string");
@ -978,10 +981,10 @@ public class ImportApplicationServiceTests {
assertEquals(expectedJSLib.getDocsUrl(), importedJSLib.getDocsUrl());
assertEquals(expectedJSLib.getVersion(), importedJSLib.getVersion());
assertEquals(expectedJSLib.getDefs(), importedJSLib.getDefs());
assertEquals(1, application.getUnpublishedCustomJSLibs().size());
assertEquals(
getDTOFromCustomJSLib(expectedJSLib),
application.getUnpublishedCustomJSLibs().toArray()[0]);
// although the imported list had only one jsLib entry, the other entry comes from ensuring an xml
// parser entry
// for backward compatibility
assertEquals(2, application.getUnpublishedCustomJSLibs().size());
assertThat(application.getName()).isEqualTo("valid_application");
assertThat(application.getWorkspaceId()).isNotNull();