feat: Import javascript libraries (#17895)

This commit is contained in:
arunvjn 2022-12-21 22:44:47 +05:30 committed by GitHub
parent 585a19401b
commit 2dc7dc90e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 3990 additions and 1454 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ const {
AggregateHelper: agHelper,
CommonLocators: locator,
EntityExplorer: ee,
LibraryInstaller: installer,
PropertyPane: propPane,
} = ObjectsRegistry;
@ -66,9 +67,26 @@ describe("Autocomplete bug fixes", function() {
it("4. feat #16426 Autocomplete for fast-xml-parser", function() {
ee.SelectEntityByName("Text1");
propPane.TypeTextIntoField("Text", "{{xmlParser.j");
agHelper.GetNAssertElementText(locator._hints, "j2xParser()");
agHelper.GetNAssertElementText(locator._hints, "j2xParser");
propPane.TypeTextIntoField("Text", "{{new xmlParser.j2xParser().p");
agHelper.GetNAssertElementText(locator._hints, "parse()");
agHelper.GetNAssertElementText(locator._hints, "parse");
});
it("5. Installed library should show up in autocomplete", function() {
ee.ExpandCollapseEntity("Libraries");
installer.openInstaller();
installer.installLibrary("uuidjs", "UUID");
installer.closeInstaller();
ee.SelectEntityByName("Text1");
propPane.TypeTextIntoField("Text", "{{UUI");
agHelper.GetNAssertElementText(locator._hints, "UUID");
});
it("6. No autocomplete for Removed libraries", function() {
ee.RenameEntityFromExplorer("Text1Copy", "UUIDTEXT");
installer.uninstallLibrary("uuidjs");
propPane.TypeTextIntoField("Text", "{{UUID.");
agHelper.AssertElementAbsence(locator._hints);
});
});

View File

@ -11,7 +11,7 @@ describe("Entity explorer context menu should hide on scrolling", function() {
// Setup to make the explorer scrollable
ee.ExpandCollapseEntity("Queries/JS");
ee.ExpandCollapseEntity("Datasources");
agHelper.ContainsNClick("Dependencies");
agHelper.ContainsNClick("Libraries");
dataSources.NavigateToDSCreateNew();
agHelper.GetNClick(dataSources._mockDB("Users"));
cy.wait("@getMockDb").then(($createdMock) => {

View File

@ -0,0 +1,36 @@
import { WIDGET } from "../../../../locators/WidgetLocators";
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const explorer = ObjectsRegistry.EntityExplorer;
const installer = ObjectsRegistry.LibraryInstaller;
const aggregateHelper = ObjectsRegistry.AggregateHelper;
const homePage = ObjectsRegistry.HomePage;
describe("Tests JS Libraries", () => {
it("1. Validates Library install/uninstall", () => {
explorer.ExpandCollapseEntity("Libraries");
installer.openInstaller();
installer.installLibrary("uuidjs", "UUID");
installer.uninstallLibrary("uuidjs");
installer.assertUnInstall("uuidjs");
});
it("2. Checks for naming collision", () => {
explorer.DragDropWidgetNVerify(WIDGET.TABLE, 200, 200);
explorer.NavigateToSwitcher("explorer");
explorer.RenameEntityFromExplorer("Table1", "jsonwebtoken");
explorer.ExpandCollapseEntity("Libraries");
installer.openInstaller();
installer.installLibrary("jsonwebtoken", "jsonwebtoken", false);
aggregateHelper.AssertContains("Name collision detected: jsonwebtoken");
});
it("3. Checks installation in exported app", () => {
homePage.NavigateToHome();
homePage.ImportApp("library_export.json");
aggregateHelper.AssertContains("true");
});
it("4. Checks installation in duplicated app", () => {
homePage.NavigateToHome();
homePage.DuplicateApplication("Library_export");
aggregateHelper.AssertContains("true");
});
});

View File

@ -6,7 +6,9 @@ const jsEditor = ObjectsRegistry.JSEditor,
apiPage = ObjectsRegistry.ApiPage,
agHelper = ObjectsRegistry.AggregateHelper,
dataSources = ObjectsRegistry.DataSources,
propPane = ObjectsRegistry.PropertyPane;
propPane = ObjectsRegistry.PropertyPane,
installer = ObjectsRegistry.LibraryInstaller,
home = ObjectsRegistry.HomePage;
const successMessage = "Successful Trigger";
const errorMessage = "Unsuccessful Trigger";
@ -271,7 +273,7 @@ describe("Linting", () => {
apiPage.CreateAndFillApi("https://jsonplaceholder.typicode.com/");
createMySQLDatasourceQuery();
agHelper.RefreshPage();//Since this seems failing a bit
agHelper.RefreshPage(); //Since this seems failing a bit
clickButtonAndAssertLintError(false);
});
@ -291,4 +293,48 @@ describe("Linting", () => {
// expect no lint error
agHelper.AssertElementAbsence(locator._lintErrorElement);
});
it("9. Shows lint errors for usage of library that are not installed yet", () => {
const JS_OBJECT_WITH_LIB_API = `export default {
myFun1: () => {
return UUID.generate();
},
}`;
jsEditor.CreateJSObject(JS_OBJECT_WITH_LIB_API, {
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
});
agHelper.AssertElementExist(locator._lintErrorElement);
ee.ExpandCollapseEntity("Libraries");
// install the library
installer.openInstaller();
installer.installLibrary("uuidjs", "UUID");
installer.closeInstaller();
agHelper.AssertElementAbsence(locator._lintErrorElement);
installer.uninstallLibrary("uuidjs");
agHelper.AssertElementExist(locator._lintErrorElement);
agHelper.Sleep(2000);
installer.openInstaller();
installer.installLibrary("uuidjs", "UUID");
installer.closeInstaller();
home.NavigateToHome();
home.CreateNewApplication();
jsEditor.CreateJSObject(JS_OBJECT_WITH_LIB_API, {
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
});
agHelper.AssertElementExist(locator._lintErrorElement);
});
});

View File

@ -12,6 +12,7 @@ import { DeployMode } from "../Pages/DeployModeHelper";
import { GitSync } from "../Pages/GitSync";
import { FakerHelper } from "../Pages/FakerHelper";
import { DebuggerHelper } from "../Pages/DebuggerHelper";
import { LibraryInstaller } from "../Pages/LibraryInstaller";
import { AppSettings } from "../Pages/AppSettings/AppSettings";
import { GeneralSettings } from "../Pages/AppSettings/GeneralSettings";
import { PageSettings } from "../Pages/AppSettings/PageSettings";
@ -161,6 +162,14 @@ export class ObjectsRegistry {
}
return ObjectsRegistry.themeSettings__;
}
private static LibraryInstaller__: LibraryInstaller;
static get LibraryInstaller(): LibraryInstaller {
if (ObjectsRegistry.LibraryInstaller__ === undefined) {
ObjectsRegistry.LibraryInstaller__ = new LibraryInstaller();
}
return ObjectsRegistry.LibraryInstaller__;
}
}
export const initLocalstorageRegistry = () => {

View File

@ -0,0 +1,62 @@
import { ObjectsRegistry } from "../Objects/Registry";
export class LibraryInstaller {
private _aggregateHelper = ObjectsRegistry.AggregateHelper;
private _installer_trigger_locator = ".t--entity-add-btn.group.libraries";
private _installer_close_locator = ".t--close-installer";
private getLibraryLocatorInExplorer(libraryName: string) {
return `.t--installed-library-${libraryName}`;
}
private getLibraryCardLocator(libraryName: string) {
return `div.library-card.t--${libraryName}`;
}
public openInstaller() {
this._aggregateHelper.GetNClick(this._installer_trigger_locator);
}
public closeInstaller() {
this._aggregateHelper.GetNClick(this._installer_close_locator);
}
public installLibrary(
libraryName: string,
accessor: string,
checkIfSuccessful = true,
) {
cy.get(this.getLibraryCardLocator(libraryName))
.find(".t--download")
.click();
if (checkIfSuccessful) this.assertInstall(libraryName, accessor);
}
private assertInstall(libraryName: string, accessor: string) {
this._aggregateHelper.AssertContains(
`Installation Successful. You can access the library via ${accessor}`,
);
cy.get(this.getLibraryCardLocator(libraryName))
.find(".installed")
.should("be.visible");
this._aggregateHelper.AssertElementExist(
this.getLibraryLocatorInExplorer(libraryName),
);
}
public uninstallLibrary(libraryName: string) {
cy.get(this.getLibraryLocatorInExplorer(libraryName))
.realHover()
.find(".t--uninstall-library")
.click();
}
public assertUnInstall(libraryName: string) {
this._aggregateHelper.AssertContains(
`${libraryName} is uninstalled successfully.`,
);
this._aggregateHelper.AssertElementAbsence(
this.getLibraryLocatorInExplorer(libraryName),
);
}
}

View File

@ -17,7 +17,7 @@ module.exports = {
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"],
moduleDirectories: ["node_modules", "src", "test"],
transformIgnorePatterns: [
"<rootDir>/node_modules/(?!codemirror|design-system|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github|lodash-es|@draft-js-plugins|react-documents)",
"<rootDir>/node_modules/(?!codemirror|design-system|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github|lodash-es|@draft-js-plugins|react-documents|linkedom)",
],
moduleNameMapper: {
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",

View File

@ -70,6 +70,7 @@
"jshint": "^2.13.4",
"klona": "^2.0.5",
"libphonenumber-js": "^1.9.44",
"linkedom": "^0.14.20",
"lint-staged": "^13.0.3",
"localforage": "^1.7.3",
"lodash": "^4.17.21",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,30 @@
import { APP_MODE } from "entities/App";
import { TJSLibrary } from "workers/common/JSLibrary";
import Api from "./Api";
export default class LibraryApi extends Api {
static base_url = "v1/libraries";
static getUpdateLibraryBaseURL = (applicationId: string) =>
`${LibraryApi.base_url}/${applicationId}`;
static addLibrary(
applicationId: string,
library: Partial<TJSLibrary> & { defs: string },
) {
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/add";
return Api.patch(url, library);
}
static removeLibrary(applicationId: string, library: Partial<TJSLibrary>) {
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/remove";
return Api.patch(url, library);
}
static getLibraries(applicationId: string, mode: APP_MODE) {
const url = `${LibraryApi.getUpdateLibraryBaseURL(applicationId)}${
mode === APP_MODE.PUBLISHED ? "/view" : ""
}`;
return Api.get(url);
}
}

View File

@ -18,6 +18,15 @@ export const ReduxSagaChannels = {
};
export const ReduxActionTypes = {
TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
INSTALL_LIBRARY_SUCCESS: "INSTALL_LIBRARY_SUCCESS",
UNINSTALL_LIBRARY_INIT: "UNINSTALL_LIBRARY_INIT",
UNINSTALL_LIBRARY_SUCCESS: "UNINSTALL_LIBRARY_SUCCESS",
GIT_DISCARD_CHANGES_SUCCESS: "GIT_DISCARD_CHANGES_SUCCESS",
GIT_DISCARD_CHANGES: "GIT_DISCARD_CHANGES",
DELETE_BRANCH_INIT: "DELETE_BRANCH_INIT",
@ -734,6 +743,8 @@ export const ReduxActionTypes = {
SET_LINT_ERRORS: "SET_LINT_ERRORS",
SET_AUTO_HEIGHT_WITH_LIMITS_CHANGING: "SET_AUTO_HEIGHT_WITH_LIMITS_CHANGING",
PROCESS_AUTO_HEIGHT_UPDATES: "PROCESS_AUTO_HEIGHT_UPDATES",
LINT_TREE: "LINT_TREE",
UPDATE_LINT_GLOBALS: "UPDATE_LINT_GLOBALS",
REMOVE_TEMP_DATASOURCE_SUCCESS: "REMOVE_TEMP_DATASOURCE_SUCCESS",
SET_DATASOURCE_SAVE_ACTION_FLAG: "SET_DATASOURCE_SAVE_ACTION_FLAG",
SET_DATASOURCE_SAVE_ACTION_FROM_POPUP_FLAG:
@ -909,6 +920,9 @@ export const ReduxActionErrorTypes = {
GET_TEMPLATE_ERROR: "GET_TEMPLATE_ERROR",
GET_TEMPLATE_FILTERS_ERROR: "GET_TEMPLATE_FILTERS_ERROR",
FETCH_CURRENT_TENANT_CONFIG_ERROR: "FETCH_CURRENT_TENANT_CONFIG_ERROR",
INSTALL_LIBRARY_FAILED: "INSTALL_LIBRARY_FAILED",
UNINSTALL_LIBRARY_FAILED: "UNINSTALL_LIBRARY_FAILED",
FETCH_JS_LIBRARIES_FAILED: "FETCH_JS_LIBRARIES_FAILED",
};
export const ReduxFormActionTypes = {

View File

@ -1121,7 +1121,7 @@ export const API_PANE_NO_BODY = () => "This request does not have a body";
export const TABLE_WIDGET_TOTAL_RECORD_TOOLTIP = () =>
"It stores the total no. of rows in the table. Helps in calculating the no. of pages that further allows to enable or disable the next/previous control in pagination.";
export const CREATE_DATASOURCE_TOOLTIP = () => "Add a new datasource";
export const ADD_QUERY_JS_TOOLTIP = () => "Create New";
export const ADD_QUERY_JS_TOOLTIP = () => "Add a new query / JS Object";
// Add datasource
export const GENERATE_APPLICATION_TITLE = () => "Generate Page";
@ -1420,6 +1420,39 @@ export const ALERT_STYLE_OPTIONS = [
{ label: "Warning", value: "'warning'", id: "warning" },
];
export const customJSLibraryMessages = {
ADD_JS_LIBRARY: () => "Add JS Libraries",
REC_LIBRARY: () => "Recommended Libraries",
INSTALLATION_SUCCESSFUL: (accessor: string) =>
`Installation Successful. You can access the library via ${accessor}`,
INSTALLATION_FAILED: () => "Installation failed",
INSTALLED_ALREADY: (accessor: string) =>
`This library is installed already. You could access it via ${accessor}.`,
UNINSTALL_FAILED: (name: string) =>
`Couldn't uninstall ${name}. Please try again after sometime.`,
UNINSTALL_SUCCESS: (accessor: string) =>
`${accessor} is uninstalled successfully.`,
LEARN_MORE_DESC: () => "Learn more about Custom JS Libraries",
UNSUPPORTED_LIB: () => `Library is unsupported`,
UNSUPPORTED_LIB_DESC: () =>
`Unfortunately, this library cannot be supported due to platform limitations. Please try installing a different library.`,
LEARN_MORE: () => `Learn more`,
REPORT_ISSUE: () => `Report issue`,
AUTOCOMPLETE_FAILED: (name: string) =>
`Code completion for ${name} will not work.`,
CLIENT_LOAD_FAILED: (url: string) => `Failed to load the script at ${url}.`,
LIB_OVERRIDE_ERROR: (
name: string,
) => `The library ${name} is already installed.
If you are trying to install a different version, uninstall the library first.`,
DEFS_FAILED_ERROR: (name: string) =>
`Failed to generate autocomplete definitions for ${name}.`,
IMPORT_URL_ERROR: (url: string) =>
`The script at ${url} cannot be installed.`,
NAME_COLLISION_ERROR: (accessors: string) =>
`Name collision detected: ${accessors}`,
};
export const USAGE_AND_BILLING = {
usage: () => "Usage",
billing: () => "Billing",

View File

@ -67,6 +67,7 @@ import tenantReducer, {
} from "@appsmith/reducers/tenantReducer";
import { FocusHistoryState } from "reducers/uiReducers/focusHistoryReducer";
import { EditorContextState } from "reducers/uiReducers/editorContextReducer";
import { LibraryState } from "reducers/uiReducers/libraryReducer";
import { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
import { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
@ -129,6 +130,7 @@ export interface AppState {
appSettingsPane: AppSettingsPaneReduxState;
focusHistory: FocusHistoryState;
editorContext: EditorContextState;
libraries: LibraryState;
autoHeightUI: AutoHeightUIState;
};
entities: {

View File

@ -40,8 +40,10 @@ import SuperUserSagas from "@appsmith/sagas/SuperUserSagas";
import NavigationSagas from "sagas/NavigationSagas";
import editorContextSagas from "sagas/editorContextSagas";
import PageVisibilitySaga from "sagas/PageVisibilitySagas";
import JSLibrarySaga from "sagas/JSLibrarySaga";
import AutoHeightSagas from "sagas/autoHeightSagas";
import tenantSagas from "@appsmith/sagas/tenantSagas";
import LintingSaga from "sagas/LintingSagas";
export const sagas = [
initSagas,
@ -88,4 +90,6 @@ export const sagas = [
PageVisibilitySaga,
AutoHeightSagas,
tenantSagas,
JSLibrarySaga,
LintingSaga,
];

View File

@ -47,6 +47,10 @@ import AppEngine, {
PluginFormConfigsNotFoundError,
PluginsNotFoundError,
} from ".";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
import { selectFeatureFlags } from "selectors/usersSelectors";
import FeatureFlags from "entities/FeatureFlags";
export default class AppEditorEngine extends AppEngine {
constructor(mode: APP_MODE) {
@ -71,6 +75,7 @@ export default class AppEditorEngine extends AppEngine {
public *setupEngine(payload: AppEnginePayload): any {
yield* super.setupEngine.call(this, payload);
yield put(resetEditorSuccess());
CodemirrorTernService.resetServer();
}
public startPerformanceTracking() {
@ -113,6 +118,12 @@ export default class AppEditorEngine extends AppEngine {
ReduxActionErrorTypes.FETCH_PAGE_ERROR,
];
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
if (featureFlags.CUSTOM_JS_LIBRARY) {
initActionsCalls.push(fetchJSLibraries(applicationId));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
}
const allActionCalls: boolean = yield call(
failFastApiCalls,
initActionsCalls,

View File

@ -17,12 +17,15 @@ import {
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { APP_MODE } from "entities/App";
import { call, put } from "redux-saga/effects";
import { call, put, select } from "redux-saga/effects";
import { failFastApiCalls } from "sagas/InitSagas";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import AppEngine, { ActionsNotFoundError, AppEnginePayload } from ".";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import FeatureFlags from "entities/FeatureFlags";
import { selectFeatureFlags } from "selectors/usersSelectors";
export default class AppViewerEngine extends AppEngine {
constructor(mode: APP_MODE) {
@ -67,28 +70,42 @@ export default class AppViewerEngine extends AppEngine {
}
*loadAppEntities(toLoadPageId: string, applicationId: string): any {
const initActionsCalls: any = [
fetchActionsForView({ applicationId }),
fetchJSCollectionsForView({ applicationId }),
fetchSelectedAppThemeAction(applicationId),
fetchAppThemesAction(applicationId),
fetchPublishedPage(toLoadPageId, true, true),
];
const successActionEffects = [
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionTypes.FETCH_APP_THEMES_SUCCESS,
ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS,
fetchPublishedPageSuccess().type,
];
const failureActionEffects = [
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
ReduxActionErrorTypes.FETCH_APP_THEMES_ERROR,
ReduxActionErrorTypes.FETCH_SELECTED_APP_THEME_ERROR,
ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR,
];
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
if (featureFlags.CUSTOM_JS_LIBRARY) {
initActionsCalls.push(fetchJSLibraries(applicationId));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(
ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
);
}
const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
[
fetchActionsForView({ applicationId }),
fetchJSCollectionsForView({ applicationId }),
fetchSelectedAppThemeAction(applicationId),
fetchAppThemesAction(applicationId),
fetchPublishedPage(toLoadPageId, true, true),
],
[
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionTypes.FETCH_JS_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionTypes.FETCH_APP_THEMES_SUCCESS,
ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS,
fetchPublishedPageSuccess().type,
],
[
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_VIEW_MODE_ERROR,
ReduxActionErrorTypes.FETCH_APP_THEMES_ERROR,
ReduxActionErrorTypes.FETCH_SELECTED_APP_THEME_ERROR,
ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR,
],
initActionsCalls,
successActionEffects,
failureActionEffects,
);
if (!resultOfPrimaryCalls)

View File

@ -8,6 +8,7 @@ type FeatureFlags = {
CONTEXT_SWITCHING?: boolean;
USAGE?: boolean;
DATASOURCE_ENVIRONMENTS?: boolean;
CUSTOM_JS_LIBRARY?: boolean;
};
export default FeatureFlags;

View File

@ -9,7 +9,7 @@ import styled from "styled-components";
import Divider from "components/editorComponents/Divider";
import Search from "./ExplorerSearch";
import { NonIdealState, Classes } from "@blueprintjs/core";
import JSDependencies from "./JSDependencies";
import JSDependencies from "./Libraries";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";

View File

@ -26,7 +26,7 @@ import { TOOLTIP_HOVER_ON_DELAY } from "constants/AppConstants";
import { EntityClassNames } from "../Entity";
import { TooltipComponent } from "design-system";
import {
ADD_QUERY_JS_BUTTON,
ADD_QUERY_JS_TOOLTIP,
createMessage,
} from "@appsmith/constants/messages";
import { useCloseMenuOnScroll } from "../hooks";
@ -223,7 +223,7 @@ export default function ExplorerSubMenu({
className={EntityClassNames.TOOLTIP}
content={
<>
{createMessage(ADD_QUERY_JS_BUTTON)} (
{createMessage(ADD_QUERY_JS_TOOLTIP)} (
{comboHelpText[SEARCH_CATEGORY_ID.ACTION_OPERATION]})
</>
}

View File

@ -1,133 +0,0 @@
import React, { useState } from "react";
import styled from "styled-components";
import { AppIcon as Icon, Size, TooltipComponent } from "design-system";
import { Colors } from "constants/Colors";
import { BindingText } from "pages/Editor/APIEditor/CommonEditorForm";
import { extraLibraries } from "utils/DynamicBindingUtils";
import CollapseToggle from "./Entity/CollapseToggle";
import Collapse from "./Entity/Collapse";
const Wrapper = styled.div`
font-size: 14px;
`;
const ListItem = styled.li`
list-style: none;
color: ${Colors.GREY_8};
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0 12px 0 20px;
position: relative;
&:hover {
background: ${Colors.ALABASTER_ALT};
& .t--open-new-tab {
display: block;
}
& .t--package-version {
display: none;
}
}
& .t--open-new-tab {
position: absolute;
right: 8px;
display: none;
}
& .t--package-version {
display: block;
}
`;
const Name = styled.span``;
const Version = styled.span``;
const Title = styled.div`
display: grid;
grid-template-columns: 20px auto 20px;
cursor: pointer;
height: 32px;
align-items: center;
padding-right: 4px;
padding-left: 0.25rem;
font-size: 14px;
&:hover {
background: ${Colors.ALABASTER_ALT};
}
& .t--help-icon {
svg {
position: relative;
}
}
span {
color: ${Colors.GREY_800};
}
`;
function JSDependencies() {
const [isOpen, setIsOpen] = useState(false);
const openDocs = (name: string, url: string) => () => window.open(url, name);
const dependencyList = extraLibraries.map((lib) => {
return (
<ListItem
key={lib.displayName}
onClick={openDocs(lib.displayName, lib.docsURL)}
>
<Name>{lib.displayName}</Name>
<Version className="t--package-version">{lib.version}</Version>
<Icon className="t--open-new-tab" name="open-new-tab" size={Size.xxs} />
</ListItem>
);
});
const toggleDependencies = React.useCallback(
() => setIsOpen((open) => !open),
[],
);
const showDocs = React.useCallback((e: any) => {
window.open(
"https://docs.appsmith.com/v/v1.2.1/core-concepts/writing-code/ext-libraries",
"appsmith-docs:working-with-js-libraries",
);
e.stopPropagation();
e.preventDefault();
}, []);
const TooltipContent = (
<div>
<span>Access these JS libraries to transform data within </span>
<BindingText>{`{{ }}`}</BindingText>
<span>. Try </span>
<BindingText>{`{{ _.add(1,1) }}`}</BindingText>
</div>
);
return (
<Wrapper>
<Title onClick={toggleDependencies}>
<CollapseToggle
className={""}
disabled={false}
isOpen={isOpen}
isVisible={!!dependencyList}
onClick={toggleDependencies}
/>
<span className="ml-1 font-medium">Dependencies</span>
<TooltipComponent content={TooltipContent} hoverOpenDelay={200}>
<Icon
className="t--help-icon"
name="help"
onClick={showDocs}
size={Size.xxs}
/>
</TooltipComponent>
</Title>
<Collapse isOpen={isOpen} step={0}>
{dependencyList}
</Collapse>
</Wrapper>
);
}
export default React.memo(JSDependencies);

View File

@ -0,0 +1,538 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import styled from "styled-components";
import {
Button,
Category,
FormGroup,
Icon,
IconSize,
Size,
Spinner,
Text,
TextInput,
TextType,
Toaster,
Variant,
} from "design-system";
import {
createMessage,
customJSLibraryMessages,
} from "@appsmith/constants/messages";
import ProfileImage from "pages/common/ProfileImage";
import { Colors } from "constants/Colors";
import { useDispatch, useSelector } from "react-redux";
import {
selectInstallationStatus,
selectInstalledLibraries,
selectIsInstallerOpen,
selectIsLibraryInstalled,
selectQueuedLibraries,
selectStatusForURL,
} from "selectors/entitiesSelector";
import SaveSuccessIcon from "remixicon-react/CheckboxCircleFillIcon";
import { InstallState } from "reducers/uiReducers/libraryReducer";
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
import { AppState } from "@appsmith/reducers";
import {
clearInstalls,
installLibraryInit,
toggleInstaller,
} from "actions/JSLibraryActions";
import classNames from "classnames";
import { TJSLibrary } from "workers/common/JSLibrary";
import AnalyticsUtil from "utils/AnalyticsUtil";
const openDoc = (e: React.MouseEvent, url: string) => {
e.preventDefault();
e.stopPropagation();
window.open(url, "_blank");
};
const Wrapper = styled.div<{ left: number }>`
display: flex;
height: auto;
width: 400px;
max-height: 80vh;
flex-direction: column;
padding: 0 24px 4px;
position: absolute;
background: white;
z-index: 25;
left: ${(props) => props.left}px;
bottom: 15px;
.installation-header {
padding: 20px 0 0;
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 12px;
}
.search-area {
margin-bottom: 16px;
.left-icon {
margin-left: 14px;
.cs-icon {
margin-right: 0;
}
}
.bp3-form-group {
margin: 0;
.remixicon-icon {
cursor: initial;
}
}
.bp3-label {
font-size: 12px;
}
display: flex;
flex-direction: column;
.search-bar {
margin-bottom: 8px;
}
}
.search-body {
display: flex;
flex-direction: column;
.search-CTA {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.search-results {
.library-card {
gap: 8px;
padding: 8px 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
border-bottom: 1px solid var(--appsmith-color-black-100);
.description {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
font-size: 12px;
line-clamp: 2;
font-weight: 400;
-webkit-box-orient: vertical;
}
img {
cursor: initial;
}
}
.library-card.no-border {
border-bottom: none;
}
}
}
`;
const InstallationProgressWrapper = styled.div<{ addBorder: boolean }>`
border-top: ${(props) =>
props.addBorder ? `1px solid var(--appsmith-color-black-300)` : "none"};
display: flex;
flex-direction: column;
background: var(--appsmith-color-black-50);
text-overflow: ellipsis;
padding: 8px 8px 12px;
.install-url {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
}
.error-card {
display: flex;
padding: 10px;
flex-direction: row;
background: #ffe9e9;
.unsupported {
line-height: 17px;
.header {
font-size: 13px;
font-weight: 600;
color: #393939;
}
.body {
font-size: 12px;
font-weight: 400;
}
}
}
`;
const StatusIconWrapper = styled.div<{
addHoverState: boolean;
}>`
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: initial;
.failed {
svg {
cursor: initial;
}
}}
${(props) =>
props.addHoverState
? `
&:hover {
cursor: pointer;
background: ${Colors.SHARK2} !important;
svg {
path {
fill: ${Colors.WHITE} !important;
}
}
}
`
: "svg { cursor: initial }"}
`;
function isValidJSFileURL(url: string) {
const JS_FILE_REGEX = /^https?:\/\/.*\.js$/;
return JS_FILE_REGEX.test(url);
}
function StatusIcon(props: {
status: InstallState;
isInstalled?: boolean;
action?: any;
}) {
const { action, isInstalled = false, status } = props;
const actionProps = useMemo(() => (action ? { onClick: action } : {}), [
action,
]);
if (status === InstallState.Success || isInstalled)
return (
<StatusIconWrapper addHoverState={false} className="installed">
<SaveSuccessIcon color={Colors.GREEN} size={18} />
</StatusIconWrapper>
);
if (status === InstallState.Failed)
return (
<StatusIconWrapper addHoverState={false} className="failed">
<Icon fillColor={Colors.GRAY} name="warning-line" size={IconSize.XL} />
</StatusIconWrapper>
);
if (status === InstallState.Queued)
return (
<StatusIconWrapper addHoverState={false} className="queued">
<Spinner />
</StatusIconWrapper>
);
return (
<StatusIconWrapper addHoverState className="t--download" {...actionProps}>
<Icon fillColor={Colors.GRAY} name="download" size={IconSize.XL} />
</StatusIconWrapper>
);
}
function ProgressTracker({
isFirst,
isLast,
status,
url,
}: {
isFirst: boolean;
isLast: boolean;
status: InstallState;
url: string;
}) {
return (
<InstallationProgressWrapper
addBorder={!isFirst}
className={classNames({
"mb-2": isLast,
})}
>
{[InstallState.Queued, InstallState.Installing].includes(status) && (
<div className="text-gray-700 text-xs">Installing...</div>
)}
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center gap-2 fw-500 text-sm">
<div className="install-url text-sm font-medium">{url}</div>
<div className="shrink-0">
<StatusIcon status={status} />
</div>
</div>
{status === InstallState.Failed && (
<div className="gap-2 error-card items-start">
<Icon name="danger" size={IconSize.XL} />
<div className="flex flex-col unsupported gap-1">
<div className="header">
{createMessage(customJSLibraryMessages.UNSUPPORTED_LIB)}
</div>
<div className="body">
{createMessage(customJSLibraryMessages.UNSUPPORTED_LIB_DESC)}
</div>
<div className="footer text-xs font-medium gap-2 flex flex-row">
<a onClick={(e) => openDoc(e, EXT_LINK.reportIssue)}>
{createMessage(customJSLibraryMessages.REPORT_ISSUE)}
</a>
<a onClick={(e) => openDoc(e, EXT_LINK.learnMore)}>
{createMessage(customJSLibraryMessages.LEARN_MORE)}
</a>
</div>
</div>
</div>
)}
</div>
</InstallationProgressWrapper>
);
}
function InstallationProgress() {
const installStatusMap = useSelector(selectInstallationStatus);
const urls = Object.keys(installStatusMap).filter(
(url) => !recommendedLibraries.find((lib) => lib.url === url),
);
if (urls.length === 0) return null;
return (
<div>
{urls.reverse().map((url, idx) => (
<ProgressTracker
isFirst={idx === 0}
isLast={idx === urls.length - 1}
key={`${url}_${idx}`}
status={installStatusMap[url]}
url={url}
/>
))}
</div>
);
}
const EXT_LINK = {
learnMore:
"https://docs.appsmith.com/core-concepts/writing-code/ext-libraries",
reportIssue: "https://github.com/appsmithorg/appsmith/issues/19037",
jsDelivr: "https://www.jsdelivr.com/",
};
export function Installer(props: { left: number }) {
const { left } = props;
const [URL, setURL] = useState("");
const [isValid, setIsValid] = useState(true);
const dispatch = useDispatch();
const installedLibraries = useSelector(selectInstalledLibraries);
const queuedLibraries = useSelector(selectQueuedLibraries);
const isOpen = useSelector(selectIsInstallerOpen);
const installerRef = useRef<HTMLDivElement>(null);
const closeInstaller = useCallback(() => {
dispatch(clearInstalls());
dispatch(toggleInstaller(false));
}, []);
const handleOutsideClick = useCallback((e: MouseEvent) => {
const paths = e.composedPath();
if (
installerRef &&
installerRef.current &&
!paths?.includes(installerRef.current)
)
closeInstaller();
}, []);
useEffect(() => {
document.addEventListener("mousedown", handleOutsideClick);
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
};
}, [isOpen]);
const updateURL = useCallback((value: string) => {
setURL(value);
}, []);
const validate = useCallback((text) => {
const isValid = !text || isValidJSFileURL(text);
setIsValid(isValid);
return {
isValid,
message: isValid ? "" : "Please enter a valid URL",
};
}, []);
useEffect(() => {
URL &&
AnalyticsUtil.logEvent("EDIT_LIBRARY_URL", { url: URL, valid: isValid });
}, [URL, isValid]);
const installLibrary = useCallback(
(lib?: Partial<TJSLibrary>) => {
const url = lib?.url || URL;
const isQueued = queuedLibraries.find((libURL) => libURL === url);
if (isQueued) return;
const libInstalled = installedLibraries.find((lib) => lib.url === url);
if (libInstalled) {
Toaster.show({
text: createMessage(
customJSLibraryMessages.INSTALLED_ALREADY,
libInstalled.accessor,
),
variant: Variant.info,
});
return;
}
dispatch(
installLibraryInit({
url,
name: lib?.name,
}),
);
},
[URL, installedLibraries, queuedLibraries],
);
return !isOpen ? null : (
<Wrapper className="bp3-popover" left={left} ref={installerRef}>
<div className="installation-header">
<Text type={TextType.H1} weight={"bold"}>
{createMessage(customJSLibraryMessages.ADD_JS_LIBRARY)}
</Text>
<Icon
className="t--close-installer"
fillColor={Colors.GRAY}
name="close-modal"
onClick={closeInstaller}
size={IconSize.XXL}
/>
</div>
<div className="search-area t--library-container">
<div className="flex flex-row gap-2 justify-between items-end">
<FormGroup className="flex-1" label={"Library URL"}>
<TextInput
$padding="12px"
autoFocus
data-testid="library-url"
height="30px"
label={"Library URL"}
leftIcon="link-2"
onChange={updateURL}
padding="12px"
placeholder="https://cdn.jsdelivr.net/npm/example@1.1.1/example.min.js"
validator={validate}
width="100%"
/>
</FormGroup>
<Button
category={Category.primary}
data-testid="install-library-btn"
disabled={!(URL && isValid)}
icon="download"
onClick={() => installLibrary()}
size={Size.medium}
tag="button"
text="INSTALL"
type="button"
/>
</div>
</div>
<div className="search-body overflow-auto">
<div className="search-CTA mb-3 text-xs">
<span>
Explore libraries on{" "}
<a
className="text-primary-500"
onClick={(e) => openDoc(e, EXT_LINK.jsDelivr)}
>
jsDelivr
</a>
{". "}
{createMessage(customJSLibraryMessages.LEARN_MORE_DESC)}{" "}
<a
className="text-primary-500"
onClick={(e) => openDoc(e, EXT_LINK.learnMore)}
>
here
</a>
{"."}
</span>
</div>
<InstallationProgress />
<div className="pb-2 sticky top-0 z-2 bg-white">
<Text type={TextType.P1} weight={"600"}>
{createMessage(customJSLibraryMessages.REC_LIBRARY)}
</Text>
</div>
<div className="search-results">
{recommendedLibraries.map((lib, idx) => (
<LibraryCard
isLastCard={idx === recommendedLibraries.length - 1}
key={`${idx}_${lib.name}`}
lib={lib}
onClick={() => installLibrary(lib)}
/>
))}
</div>
</div>
</Wrapper>
);
}
function LibraryCard({
isLastCard,
lib,
onClick,
}: {
lib: typeof recommendedLibraries[0];
onClick: (url: string) => void;
isLastCard: boolean;
}) {
const status = useSelector(selectStatusForURL(lib.url));
const isInstalled = useSelector((state: AppState) =>
selectIsLibraryInstalled(state, lib.url),
);
return (
<div
className={classNames({
[`library-card t--${lib.name}`]: true,
"no-border": isLastCard,
})}
>
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row gap-2 items-center">
<Text type={TextType.P0} weight="500">
{lib.name}
</Text>
<StatusIconWrapper
addHoverState
onClick={(e) => openDoc(e, lib.docsURL)}
>
<Icon
fillColor={Colors.GRAY}
name="share-2"
size={IconSize.SMALL}
/>
</StatusIconWrapper>
</div>
<div className="mr-2">
<StatusIcon
action={onClick}
isInstalled={isInstalled}
status={status}
/>
</div>
</div>
<div className="flex flex-row description">{lib.description}</div>
<div className="flex flex-row items-center gap-1">
<ProfileImage size={20} source={lib.icon} />
<Text type={TextType.P3}>{lib.author}</Text>
</div>
</div>
);
}

View File

@ -0,0 +1,152 @@
import React from "react";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { Provider } from "react-redux";
import store from "store";
import { ThemeProvider } from "styled-components";
import { lightTheme } from "selectors/themeSelectors";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { Installer } from "pages/Editor/Explorer/Libraries/Installer";
export const fetchApplicationMockResponse = {
responseMeta: {
status: 200,
success: true,
},
data: {
application: {
id: "605c435a91dea93f0eaf91b8",
name: "My Application",
slug: "my-application",
workspaceId: "",
evaluationVersion: 1,
appIsExample: false,
gitApplicationMetadata: undefined,
applicationVersion: 2,
},
pages: [
{
id: "605c435a91dea93f0eaf91ba",
name: "Page1",
isDefault: true,
slug: "page-1",
},
{
id: "605c435a91dea93f0eaf91bc",
name: "Page2",
isDefault: false,
slug: "page-2",
},
],
workspaceId: "",
},
};
describe("Contains all UI tests for JS libraries", () => {
store.dispatch({
type: ReduxActionTypes.TOGGLE_INSTALLER,
payload: true,
});
afterEach(cleanup);
it("Headers should exist", () => {
render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Installer left={250} />
</ThemeProvider>
</Provider>,
);
expect(screen.getByText("Add JS Libraries")).toBeDefined();
expect(screen.getByText("Recommended Libraries")).toBeDefined();
expect(screen.getByTestId("library-url")).toBeDefined();
expect(screen.getByTestId("install-library-btn")).toBeDisabled();
});
it("Validates URL", () => {
render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Installer left={250} />
</ThemeProvider>
</Provider>,
);
const input = screen.getByTestId("library-url");
fireEvent.change(input, { target: { value: "https://valid.com/file.js" } });
expect(screen.getByTestId("install-library-btn")).toBeEnabled();
expect(screen.queryByText("Please enter a valid URL")).toBeNull();
fireEvent.change(input, { target: { value: "23" } });
expect(screen.queryByText("Please enter a valid URL")).toBeDefined();
expect(screen.getByTestId("install-library-btn")).toBeDisabled();
});
it("Renders progress bar", () => {
render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Installer left={250} />
</ThemeProvider>
</Provider>,
);
store.dispatch({
type: ReduxActionTypes.FETCH_APPLICATION_SUCCESS,
payload: {
...fetchApplicationMockResponse.data.application,
pages: fetchApplicationMockResponse.data.pages,
},
});
const input = screen.getByTestId("library-url");
fireEvent.change(input, {
target: {
value:
"https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
},
});
const installButton = screen.getByTestId("install-library-btn");
expect(installButton).toBeDefined();
fireEvent.click(installButton);
expect(
screen.queryByText(
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
),
).toBeDefined();
});
it("Progress bar should disappear once the installation succeeds or fails", () => {
render(
<Provider store={store}>
<ThemeProvider theme={lightTheme}>
<Installer left={250} />
</ThemeProvider>
</Provider>,
);
store.dispatch({
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,
payload:
"https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
});
expect(
screen.queryByText(
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
),
).toBeDefined();
store.dispatch({
type: ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
payload: {
name: "dayjs",
version: "1.11.6",
accessor: ["dayjs"],
url: "https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.6/dayjs.min.js",
},
});
expect(
screen.queryByText(
`Installing library for ${fetchApplicationMockResponse.data.application.name}`,
),
).toBeNull();
});
});

View File

@ -0,0 +1,338 @@
import React, { MutableRefObject, useCallback, useRef } from "react";
import styled from "styled-components";
import {
Icon,
IconSize,
Spinner,
Toaster,
TooltipComponent,
Variant,
} from "design-system";
import { Colors } from "constants/Colors";
import Entity, { EntityClassNames } from "../Entity";
import {
createMessage,
customJSLibraryMessages,
} from "@appsmith/constants/messages";
import { useDispatch, useSelector } from "react-redux";
import {
selectInstallationStatus,
selectIsInstallerOpen,
selectLibrariesForExplorer,
} from "selectors/entitiesSelector";
import { InstallState } from "reducers/uiReducers/libraryReducer";
import { Collapse } from "@blueprintjs/core";
import { ReactComponent as CopyIcon } from "assets/icons/menu/copy-snippet.svg";
import useClipboard from "utils/hooks/useClipboard";
import {
toggleInstaller,
uninstallLibraryInit,
} from "actions/JSLibraryActions";
import EntityAddButton from "../Entity/AddButton";
import { TOOLTIP_HOVER_ON_DELAY } from "constants/AppConstants";
import { TJSLibrary } from "workers/common/JSLibrary";
import { getPagePermissions } from "selectors/editorSelectors";
import { hasCreateActionPermission } from "@appsmith/utils/permissionHelpers";
import { selectFeatureFlags } from "selectors/usersSelectors";
import recommendedLibraries from "./recommendedLibraries";
const docsURLMap = recommendedLibraries.reduce((acc, lib) => {
acc[lib.url] = lib.docsURL;
return acc;
}, {} as Record<string, string>);
const Library = styled.li`
list-style: none;
flex-direction: column;
color: ${Colors.GRAY_700};
font-weight: 400;
display: flex;
justify-content: space-between;
cursor: pointer;
position: relative;
line-height: 17px;
padding-left: 8px;
> div:first-child {
height: 36px;
}
.share {
display: none;
width: 30px;
height: 36px;
background: transparent;
margin-left: 8px;
flex-shrink: 0;
}
&:hover {
background: ${Colors.SEA_SHELL};
& .t--open-new-tab {
display: block;
}
& .delete,
.share {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
&:hover {
background: black;
.uninstall-library,
.open-link {
color: white;
svg > path {
fill: white;
}
}
}
}
}
.loading {
display: none;
width: 30px;
height: 36px;
background: transparent;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
& .t--open-new-tab {
position: absolute;
right: 8px;
display: none;
}
.delete {
display: none;
width: 30px;
height: 36px;
background: transparent;
flex-shrink: 0;
}
& .t--package-version {
display: block;
font-size: 12px;
height: 16px;
}
.open-collapse {
transform: rotate(90deg);
}
.content {
font-size: 12px;
font-weight: 400;
padding: 4px 8px;
color: ${Colors.GRAY_700};
display: flex;
align-items: center;
gap: 4px;
width: 100%;
overflow: hidden;
.accessor {
padding-left: 8px;
flex-grow: 1;
outline: 1px solid #b3b3b3 !important;
font-size: 12px;
font-family: monospace;
background: white;
display: flex;
height: 25px;
width: calc(100% - 80px);
justify-content: space-between;
align-items: center;
color: ${Colors.ENTERPRISE_DARK};
> div {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
width: 25px;
&: hover {
background: ${Colors.SHARK2};
> svg > path {
fill: ${Colors.WHITE};
}
}
}
}
}
`;
const Name = styled.div`
display: flex;
align-items: center;
line-height: 17px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
`;
const Version = styled.div<{ version?: string }>`
flex-shrink: 0;
display: ${(props) => (props.version ? "block" : "none")};
margin: ${(props) => (props.version ? "0 8px" : "0")};
`;
const PrimaryCTA = function({ lib }: { lib: TJSLibrary }) {
const installationStatus = useSelector(selectInstallationStatus);
const dispatch = useDispatch();
const url = lib.url as string;
const uninstallLibrary = useCallback(
(e) => {
e.stopPropagation();
dispatch(uninstallLibraryInit(lib));
},
[lib],
);
if (installationStatus[url] === InstallState.Queued)
return (
<div className="loading">
<Spinner size={IconSize.MEDIUM} />
</div>
);
if (url) {
//Default libraries will not have url
return (
<div className="delete" onClick={uninstallLibrary}>
<Icon
className="uninstall-library t--uninstall-library"
name="trash-outline"
size={IconSize.MEDIUM}
/>
</div>
);
}
return null;
};
function LibraryEntity({ lib }: { lib: TJSLibrary }) {
const openDocs = useCallback(
(url?: string) => (e: React.MouseEvent) => {
e?.stopPropagation();
url && window.open(url, "_blank");
},
[],
);
const propertyRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const write = useClipboard(propertyRef);
const copyToClipboard = useCallback(() => {
write(lib.accessor[lib.accessor.length - 1]);
Toaster.show({
text: "Copied to clipboard",
variant: Variant.success,
});
}, [lib.accessor]);
const [isOpen, open] = React.useState(false);
const docsURL = docsURLMap[lib.url || ""] || lib.docsURL;
return (
<Library className={`t--installed-library-${lib.name}`}>
<div
className="flex flex-row items-center h-full"
onClick={() => open(!isOpen)}
>
<Icon
className={isOpen ? "open-collapse" : ""}
fillColor={Colors.GREY_7}
name="right-arrow-2"
size={IconSize.XXXL}
/>
<div className="flex items-center flex-start flex-1 overflow-hidden">
<Name>{lib.name}</Name>
{docsURL && (
<div className="share" onClick={openDocs(docsURL)}>
<Icon
className="open-link"
fillColor={Colors.GRAY_700}
name="share-2"
size={IconSize.SMALL}
/>
</div>
)}
</div>
<Version className="t--package-version" version={lib.version}>
{lib.version}
</Version>
<PrimaryCTA lib={lib} />
</div>
<Collapse className="text-xs" isOpen={isOpen}>
<div className="content pr-2">
Available as{" "}
<div className="accessor">
{lib.accessor[lib.accessor.length - 1]}{" "}
<div>
<CopyIcon onClick={copyToClipboard} />
</div>
</div>
</div>
</Collapse>
</Library>
);
}
function JSDependencies() {
const libraries = useSelector(selectLibrariesForExplorer);
const dependencyList = libraries.map((lib) => (
<LibraryEntity key={lib.name} lib={lib} />
));
const isOpen = useSelector(selectIsInstallerOpen);
const dispatch = useDispatch();
const pagePermissions = useSelector(getPagePermissions);
const canCreateActions = hasCreateActionPermission(pagePermissions);
const openInstaller = useCallback(() => {
dispatch(toggleInstaller(true));
}, []);
const featureFlags = useSelector(selectFeatureFlags);
return (
<Entity
className={"libraries"}
customAddButton={
<TooltipComponent
boundary="viewport"
className={EntityClassNames.TOOLTIP}
content={createMessage(customJSLibraryMessages.ADD_JS_LIBRARY)}
disabled={isOpen}
hoverOpenDelay={TOOLTIP_HOVER_ON_DELAY}
position="right"
>
<EntityAddButton
className={`${EntityClassNames.ADD_BUTTON} group libraries h-100 ${
isOpen ? "selected" : ""
}`}
onClick={openInstaller}
/>
</TooltipComponent>
}
entityId="library_section"
icon={null}
isDefaultExpanded={isOpen}
isSticky
name="Libraries"
showAddButton={canCreateActions && featureFlags?.CUSTOM_JS_LIBRARY}
step={0}
>
{dependencyList}
</Entity>
);
}
export default React.memo(JSDependencies);

View File

@ -0,0 +1,150 @@
export default [
{
name: "uuidjs",
url: "https://cdn.jsdelivr.net/npm/uuidjs@4.2.12/src/uuid.min.js",
description:
"UUID.js is a JavaScript/ECMAScript library to generate RFC 4122 compliant Universally Unique IDentifiers (UUIDs). This library supports both version 4 UUIDs (UUIDs from random numbers) and version 1 UUIDs (time-based UUIDs), and provides an object-oriented interface to print a generated or parsed UUID in a variety of forms.",
author: "LiosK",
docsURL:
"https://github.com/LiosK/UUID.js/#uuidjs---rfc-compliant-uuid-generator-for-javascript",
version: "4.2.12",
icon: "https://github.com/LiosK.png?s=20",
},
{
name: "i18next",
url: "https://cdn.jsdelivr.net/npm/i18next@22.1.4/dist/umd/i18next.min.js",
description: "i18next internationalization framework",
author: "i18next",
version: "22.1.4",
icon: "https://github.com/i18next.png?s=20",
docsURL: "https://www.i18next.com/overview/getting-started",
},
{
name: "jsonwebtoken",
description: "JSON Web Token implementation (symmetric and asymmetric)",
author: "auth0",
docsURL: "https://github.com/auth0/node-jsonwebtoken#readme",
version: "8.5.1",
url: `/libraries/jsonwebtoken@8.5.1.js`,
icon: "https://github.com/auth0.png?s=20",
},
{
name: "@supabase/supabase-js",
description: "Isomorphic Javascript client for Supabase",
author: "supabase",
docsURL: "https://supabase.com/docs/reference/javascript",
version: "2.1.0",
url:
"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.1.0/dist/umd/supabase.min.js",
icon: "https://github.com/supabase.png?s=20",
},
{
name: "@segment/analytics-next",
url:
"https://cdn.jsdelivr.net/npm/@segment/analytics-next@1.46.1/dist/umd/index.js",
description:
"Analytics Next (aka Analytics 2.0) is the latest version of Segments JavaScript SDK - enabling you to send your data to any tool without having to learn, test, or use a new API every time.",
author: "segmentio",
docsURL:
"https://github.com/segmentio/analytics-next/tree/master/packages/browser#readme",
version: "1.46.1",
icon: "https://github.com/segmentio.png?s=20",
},
{
name: "mixpanel-browser",
description: "The official Mixpanel JavaScript browser client library",
author: "mixpanel",
docsURL: "https://developer.mixpanel.com/docs/javascript",
version: "2.1.0",
url: `https://cdn.jsdelivr.net/npm/mixpanel-browser@2.45.0/dist/mixpanel.umd.js`,
icon: "https://github.com/mixpanel.png?s=20",
},
{
name: "fast-csv",
description: "CSV parser and writer",
author: "C2FO",
docsURL:
"https://c2fo.github.io/fast-csv/docs/introduction/getting-started/",
version: "4.3.6",
url: `/libraries/fast-csv@4.3.6.js`,
icon: "https://github.com/C2FO.png?s=20",
},
{
name: "ky",
description: "Tiny and elegant HTTP client based on the browser Fetch API",
author: "sindresorhus",
docsURL: "https://github.com/sindresorhus/ky#usage",
version: "0.25.0",
url: "https://www.unpkg.com/ky@0.25.0/umd.js",
icon: "https://github.com/sindresorhus.png?s=20",
},
{
name: "jspdf",
description: "PDF Document creation from JavaScript",
author: "MrRio",
docsURL: "https://raw.githack.com/MrRio/jsPDF/master/docs/index.html",
version: "2.5.1",
url: "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js",
icon: "https://github.com/MrRio.png?s=20",
},
{
name: "@amplitude/analytics-browser",
description: "Official Amplitude SDK for Web",
author: "amplitude",
docsURL:
"https://github.com/amplitude/Amplitude-TypeScript/tree/main/packages/analytics-browser#usage",
version: "1.6.1",
url:
"https://cdn.jsdelivr.net/npm/@amplitude/analytics-browser@1.6.1/lib/scripts/amplitude-min.umd.js",
icon: "https://github.com/amplitude.png?s=20",
},
{
name: "uzip-module",
description: "Module version of UZIP.js",
author: "greggman",
docsURL: "https://github.com/greggman/uzip-module#uzip-module",
version: "1.0.3",
url: `https://cdn.jsdelivr.net/npm/uzip-module@1.0.3/dist/uzip.js`,
icon: "https://github.com/greggman.png?s=20",
},
{
name: "@sentry/browser",
description: "Official Sentry SDK for browsers",
author: "getsentry",
docsURL: "https://docs.sentry.io/platforms/javascript/",
version: "7.17.3",
url: "https://browser.sentry-cdn.com/7.17.3/bundle.min.js",
icon: "https://github.com/getsentry.png?s=20",
},
{
name: "browser-image-compression",
url:
"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.0/dist/browser-image-compression.min.js",
version: "2.0.0",
author: "Donaldcwl",
docsURL:
"https://github.com/Donaldcwl/browser-image-compression/#browser-image-compression",
description: "Compress images in the browser",
icon: "https://github.com/Donaldcwl.png?s=20",
},
{
name: "crypto-js",
url:
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js",
description: "JavaScript library of crypto standards",
version: "4.1.1",
author: "brix",
docsURL: "https://github.com/brix/crypto-js/#crypto-js",
icon: "https://github.com/brix.png?s=20",
},
{
name: "jsonpath",
url: "https://cdn.jsdelivr.net/npm/jsonpath@1.1.1/jsonpath.min.js",
description:
"Query JavaScript objects with JSONPath expressions. Robust / safe JSONPath engine for Node.js",
version: "1.1.1",
author: "dchester",
docsURL: "https://github.com/dchester/jsonpath/#jsonpath",
icon: "https://github.com/dchester.png?s=20",
},
];

View File

@ -49,6 +49,7 @@ import { setExplorerPinnedAction } from "actions/explorerActions";
import { setIsGitSyncModalOpen } from "actions/gitSyncActions";
import { GitSyncModalTab } from "entities/GitSync";
import { matchBuilderPath } from "constants/routes";
import { toggleInstaller } from "actions/JSLibraryActions";
type Props = {
copySelectedWidget: () => void;
@ -77,6 +78,7 @@ type Props = {
setExplorerPinnedAction: (shouldPinned: boolean) => void;
showCommitModal: () => void;
getMousePosition: () => { x: number; y: number };
hideInstaller: () => void;
};
@HotkeysTarget
@ -109,6 +111,7 @@ class GlobalHotKeys extends React.Component<Props> {
const category = filterCategories[categoryId];
this.props.setGlobalSearchCategory(category);
this.props.hideInstaller();
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
source: "HOTKEY_COMBO",
category: category.title,
@ -359,6 +362,7 @@ class GlobalHotKeys extends React.Component<Props> {
label="Pin/Unpin Entity Explorer"
onKeyDown={() => {
this.props.setExplorerPinnedAction(!this.props.isExplorerPinned);
this.props.hideInstaller();
}}
/>
<Hotkey
@ -414,6 +418,7 @@ const mapDispatchToProps = (dispatch: any) => {
dispatch(
setIsGitSyncModalOpen({ isOpen: true, tab: GitSyncModalTab.DEPLOY }),
),
hideInstaller: () => dispatch(toggleInstaller(false)),
};
};

View File

@ -17,6 +17,7 @@ import EntityExplorerSidebar from "components/editorComponents/Sidebar";
import classNames from "classnames";
import { previewModeSelector } from "selectors/editorSelectors";
import { routeChanged } from "actions/focusHistoryActions";
import { Installer } from "pages/Editor/Explorer/Libraries/Installer";
import { getExplorerWidth } from "selectors/explorerSelector";
import { AppsmithLocationState } from "utils/history";
@ -96,6 +97,7 @@ function MainContainer() {
"transition-all transform duration-400": true,
})}
/>
<Installer left={sidebarWidth} />
</>
);
}

View File

@ -51,7 +51,13 @@ const initialState: EditorContextState = {
explorerSwitchIndex: 0,
};
const entitySections = ["Pages", "Widgets", "Queries/JS", "Datasources"];
const entitySections = [
"Pages",
"Widgets",
"Queries/JS",
"Datasources",
"Libraries",
];
export const isSubEntities = (name: string): boolean => {
return entitySections.indexOf(name) < 0;

View File

@ -42,6 +42,7 @@ import mainCanvasReducer from "./mainCanvasReducer";
import focusHistoryReducer from "./focusHistoryReducer";
import { editorContextReducer } from "./editorContextReducer";
import guidedTourReducer from "./guidedTourReducer";
import libraryReducer from "./libraryReducer";
import appSettingsPaneReducer from "./appSettingsPaneReducer";
import autoHeightUIReducer from "./autoHeightReducer";
@ -90,6 +91,7 @@ const uiReducer = combineReducers({
appSettingsPane: appSettingsPaneReducer,
focusHistory: focusHistoryReducer,
editorContext: editorContextReducer,
libraries: libraryReducer,
autoHeightUI: autoHeightUIReducer,
});

View File

@ -0,0 +1,116 @@
import { createImmerReducer } from "utils/ReducerUtils";
import {
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
import { defaultLibraries, TJSLibrary } from "workers/common/JSLibrary";
export enum InstallState {
Queued,
Installing,
Failed,
Success,
}
export type LibraryState = {
installationStatus: Record<string, InstallState>;
installedLibraries: TJSLibrary[];
isInstallerOpen: boolean;
};
const initialState = {
isInstallerOpen: false,
installationStatus: {},
installedLibraries: defaultLibraries.map((lib: TJSLibrary) => {
return {
name: lib.name,
docsURL: lib.docsURL,
version: lib.version,
accessor: lib.accessor,
};
}),
reservedNames: [],
};
const jsLibraryReducer = createImmerReducer(initialState, {
[ReduxActionTypes.INSTALL_LIBRARY_INIT]: (
state: LibraryState,
action: ReduxAction<Partial<TJSLibrary>>,
) => {
const { url } = action.payload;
state.installationStatus[url as string] =
state.installationStatus[url as string] || InstallState.Queued;
},
[ReduxActionTypes.INSTALL_LIBRARY_START]: (
state: LibraryState,
action: ReduxAction<string>,
) => {
state.installationStatus[action.payload] = InstallState.Queued;
},
[ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: (
state: LibraryState,
action: ReduxAction<{
accessor: string[];
url: string;
version: string;
}>,
) => {
const { accessor, url, version } = action.payload;
const name = accessor[accessor.length - 1] as string;
const recommendedLibrary = recommendedLibraries.find(
(lib) => lib.url === url,
);
state.installationStatus[url] = InstallState.Success;
state.installedLibraries.unshift({
name: recommendedLibrary?.name || name,
docsURL: recommendedLibrary?.url || url,
version: recommendedLibrary?.version || version,
url,
accessor,
});
},
[ReduxActionErrorTypes.INSTALL_LIBRARY_FAILED]: (
state: LibraryState,
action: ReduxAction<{ url: string }>,
) => {
state.installationStatus[action.payload.url] = InstallState.Failed;
},
[ReduxActionTypes.CLEAR_PROCESSED_INSTALLS]: (state: LibraryState) => {
for (const key in state.installationStatus) {
if (
[InstallState.Success, InstallState.Failed].includes(
state.installationStatus[key],
)
) {
delete state.installationStatus[key];
}
}
},
[ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS]: (
state: LibraryState,
action: ReduxAction<TJSLibrary[]>,
) => {
state.installedLibraries = action.payload.concat(
initialState.installedLibraries,
);
},
[ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS]: (
state: LibraryState,
action: ReduxAction<TJSLibrary>,
) => {
const uLib = action.payload;
state.installedLibraries = state.installedLibraries.filter(
(lib) => uLib.url !== lib.url,
);
},
[ReduxActionTypes.TOGGLE_INSTALLER]: (
state: LibraryState,
action: ReduxAction<boolean>,
) => {
state.isInstallerOpen = action.payload;
},
});
export default jsLibraryReducer;

View File

@ -8,6 +8,20 @@ import {
} from "entities/DataTree/actionTriggers";
import { ActionValidationError } from "sagas/ActionExecution/errorUtils";
import { isBase64String, isUrlString } from "./downloadActionUtils";
import { isBlobUrl } from "utils/AppsmithUtils";
function downloadBlobURL(url: string, name: string) {
const ele = document.createElement("a");
ele.href = url;
ele.download = name;
ele.style.display = "none";
document.body.appendChild(ele);
ele.click();
setTimeout(() => {
URL.revokeObjectURL(url);
document.body.removeChild(ele);
});
}
export default async function downloadSaga(
action: DownloadActionDescription["payload"],
@ -21,6 +35,13 @@ export default async function downloadSaga(
getType(name),
);
}
if (isBlobUrl(data)) {
downloadBlobURL(data, name);
AppsmithConsole.info({
text: `download('${data}', '${name}', '${type}') was triggered`,
});
return;
}
const dataType = getType(data);
if (dataType === Types.ARRAY || dataType === Types.OBJECT) {
const jsonString = JSON.stringify(data, null, 2);

View File

@ -27,9 +27,12 @@ import WidgetFactory, { WidgetTypeConfigMap } from "utils/WidgetFactory";
import { GracefulWorkerService } from "utils/WorkerUtil";
import {
EvalError,
EVAL_WORKER_ACTIONS,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import {
EVAL_WORKER_ACTIONS,
MAIN_THREAD_ACTION,
} from "workers/Evaluation/evalWorkerActions";
import log from "loglevel";
import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
@ -103,11 +106,12 @@ import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsRe
import { AppTheme } from "entities/AppTheming";
import { ActionValidationConfigMap } from "constants/PropertyControlConstants";
import { storeLogs, updateTriggerMeta } from "./DebuggerSagas";
import { lintTreeSaga, lintWorker } from "./LintingSagas";
import { lintWorker } from "./LintingSagas";
import {
EvalTreeRequestData,
EvalTreeResponseData,
} from "workers/Evaluation/types";
import { MessageType, TMessage } from "utils/MessageUtil";
const evalWorker = new GracefulWorkerService(
new Worker(
@ -121,10 +125,21 @@ const evalWorker = new GracefulWorkerService(
let widgetTypeConfigMap: WidgetTypeConfigMap;
function* evaluateTreeSaga(
/**
* This saga is responsible for evaluating the data tree
* @param postEvalActions
* @param shouldReplay
* @param requiresLinting
* @param forceEvaluation - if true, will re-evaluate the entire tree
* @returns
* @example
* yield call(evaluateTreeSaga, postEvalActions, shouldReplay, requiresLinting, forceEvaluation)
*/
export function* evaluateTreeSaga(
postEvalActions?: Array<AnyReduxAction>,
shouldReplay = true,
requiresLinting = false,
forceEvaluation = false,
) {
const allActionValidationConfig: {
[actionId: string]: ActionValidationConfigMap;
@ -146,6 +161,7 @@ function* evaluateTreeSaga(
shouldReplay,
allActionValidationConfig,
requiresLinting: isEditMode && requiresLinting,
forceEvaluation,
};
const workerResponse: EvalTreeResponseData = yield call(
@ -270,8 +286,8 @@ export function* evaluateAndExecuteDynamicTrigger(
) {
const unEvalTree: DataTree = yield select(getUnevaluatedDataTree);
log.debug({ execute: dynamicTrigger });
const { isFinishedChannel } = yield call(
evalWorker.duplexRequest,
const response: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TRIGGER,
{
unEvalTree,
@ -283,122 +299,90 @@ export function* evaluateAndExecuteDynamicTrigger(
},
);
let keepAlive = true;
const { logs = [], errors = [], triggers = [] } = response as any;
yield call(updateTriggerMeta, triggerMeta, dynamicTrigger);
yield call(
storeLogs,
logs,
triggerMeta.source?.name || triggerMeta.triggerPropertyName || "",
eventType === EventType.ON_JS_FUNCTION_EXECUTE
? ENTITY_TYPE.JSACTION
: ENTITY_TYPE.WIDGET,
triggerMeta.source?.id || "",
);
while (keepAlive) {
const { requestData } = yield take(isFinishedChannel);
log.debug({ requestData, eventType, triggerMeta, dynamicTrigger });
if (requestData.finished) {
keepAlive = false;
const { result } = requestData;
yield call(updateTriggerMeta, triggerMeta, dynamicTrigger);
// Check for any logs in the response and store them in the redux store
if (
!!result &&
result.hasOwnProperty("logs") &&
!!result.logs &&
result.logs.length
) {
yield call(
storeLogs,
result.logs,
triggerMeta.source?.name || triggerMeta.triggerPropertyName || "",
eventType === EventType.ON_JS_FUNCTION_EXECUTE
? ENTITY_TYPE.JSACTION
: ENTITY_TYPE.WIDGET,
triggerMeta.source?.id || "",
);
}
/* Handle errors during evaluation
* A finish event with errors means that the error was not caught by the user code.
* We raise an error telling the user that an uncaught error has occurred
* */
if (
!!result &&
result.hasOwnProperty("errors") &&
!!result.errors &&
result.errors.length
) {
if (
result.errors[0].errorMessage !==
"UncaughtPromiseRejection: User cancelled action execution"
) {
throw new UncaughtPromiseError(result.errors[0].errorMessage);
}
}
// It is possible to get a few triggers here if the user
// still uses the old way of action runs and not promises. For that we
// need to manually execute these triggers outside the promise flow
const { triggers } = result;
if (triggers && triggers.length) {
log.debug({ triggers });
yield all(
triggers.map((trigger: ActionDescription) =>
call(executeActionTriggers, trigger, eventType, triggerMeta),
),
);
}
// Return value of a promise is returned
isFinishedChannel.close();
return result;
yield call(evalErrorHandler, errors);
if (errors.length) {
if (
errors[0].errorMessage !==
"UncaughtPromiseRejection: User cancelled action execution"
) {
throw new UncaughtPromiseError(errors[0].errorMessage);
}
yield call(evalErrorHandler, requestData.errors);
isFinishedChannel.close();
}
log.debug({ triggers });
yield all(
triggers.map((trigger: ActionDescription) =>
call(executeActionTriggers, trigger, eventType, triggerMeta),
),
);
return response;
}
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
while (true) {
const request: TMessage<any> = yield take(listenerChannel);
yield spawn(handleEvalWorkerMessage, request);
}
}
export function* executeDynamicTriggerRequest(
mainThreadRequestChannel: Channel<any>,
) {
while (true) {
const { mainThreadResponseChannel, requestData, requestId } = yield take(
mainThreadRequestChannel,
);
log.debug({ requestData });
if (requestData?.logs) {
const { eventType, triggerMeta } = requestData;
export function* handleEvalWorkerMessage(message: TMessage<any>) {
const { body, messageType } = message;
const { data, method } = body;
switch (method) {
case MAIN_THREAD_ACTION.LINT_TREE: {
yield put({
type: ReduxActionTypes.LINT_TREE,
payload: {
pathsToLint: data.lintOrder,
unevalTree: data.unevalTree,
},
});
break;
}
case MAIN_THREAD_ACTION.PROCESS_LOGS: {
const { logs = [], triggerMeta, eventType } = data;
yield call(
storeLogs,
requestData.logs,
logs,
triggerMeta?.source?.name || triggerMeta?.triggerPropertyName || "",
eventType === EventType.ON_JS_FUNCTION_EXECUTE
? ENTITY_TYPE.JSACTION
: ENTITY_TYPE.WIDGET,
triggerMeta?.source?.id || "",
);
break;
}
if (requestData?.trigger) {
// if we have found a trigger, we need to execute it and respond back
log.debug({ trigger: requestData.trigger });
yield spawn(
case MAIN_THREAD_ACTION.PROCESS_TRIGGER: {
const { eventType, trigger, triggerMeta } = data;
log.debug({ trigger: data.trigger });
const result: ResponsePayload = yield call(
executeTriggerRequestSaga,
requestId,
requestData,
requestData.eventType,
mainThreadResponseChannel,
requestData.triggerMeta,
trigger,
eventType,
triggerMeta,
);
}
if (requestData.type === EVAL_WORKER_ACTIONS.LINT_TREE) {
yield spawn(lintTreeSaga, {
pathsToLint: requestData.lintOrder,
unevalTree: requestData.unevalTree,
});
}
if (requestData?.errors) {
yield call(evalErrorHandler, requestData.errors);
if (messageType === MessageType.REQUEST)
yield call(evalWorker.respond, message.messageId, result);
break;
}
}
yield call(evalErrorHandler, data?.errors || []);
}
interface ResponsePayload {
data: {
subRequestId: string;
reason?: string;
resolve?: unknown;
};
@ -411,17 +395,14 @@ interface ResponsePayload {
* resolve or reject it with the data the execution has provided
*/
function* executeTriggerRequestSaga(
requestId: string,
requestData: { trigger: ActionDescription; subRequestId: string },
trigger: ActionDescription,
eventType: EventType,
responseFromExecutionChannel: Channel<unknown>,
triggerMeta: TriggerMeta,
) {
const responsePayload: ResponsePayload = {
data: {
resolve: undefined,
reason: undefined,
subRequestId: requestData.subRequestId,
},
success: false,
eventType,
@ -429,7 +410,7 @@ function* executeTriggerRequestSaga(
try {
responsePayload.data.resolve = yield call(
executeActionTriggers,
requestData.trigger,
trigger,
eventType,
triggerMeta,
);
@ -442,11 +423,7 @@ function* executeTriggerRequestSaga(
responsePayload.data.reason = { message: error.message };
responsePayload.success = false;
}
responseFromExecutionChannel.put({
method: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
requestId: requestId,
...responsePayload,
});
return responsePayload;
}
export function* clearEvalCache() {
@ -497,17 +474,15 @@ export function* executeFunction(
},
);
const { logs } = response;
const { logs = [] } = response;
// Check for any logs in the response and store them in the redux store
if (!!logs && logs.length > 0) {
yield call(
storeLogs,
logs,
collectionName + "." + action.name,
ENTITY_TYPE.JSACTION,
collectionId,
);
}
yield call(
storeLogs,
logs,
collectionName + "." + action.name,
ENTITY_TYPE.JSACTION,
collectionId,
);
}
const { errors, result } = response;
@ -611,16 +586,16 @@ function getPostEvalActions(
return postEvalActions;
}
function* evaluationChangeListenerSaga() {
function* evaluationChangeListenerSaga(): any {
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
const [{ mainThreadRequestChannel }] = yield all([
const [evalWorkerListenerChannel] = yield all([
call(evalWorker.start),
call(lintWorker.start),
]);
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP);
yield spawn(executeDynamicTriggerRequest, mainThreadRequestChannel);
yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
const initAction: {
@ -654,7 +629,7 @@ export function* evaluateSnippetSaga(action: any) {
let { expression } = action.payload;
const { dataType, isTrigger } = action.payload;
if (isTrigger) {
expression = `function() { ${expression} }`;
expression = `(function() { ${expression} })()`;
}
const workerResponse: {
errors: any;
@ -663,7 +638,6 @@ export function* evaluateSnippetSaga(action: any) {
} = yield call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_EXPRESSION, {
expression,
dataType,
isTrigger,
});
const { errors, result, triggers } = workerResponse;
if (triggers && triggers.length > 0) {
@ -830,3 +804,5 @@ export default function* evaluationSagaListeners() {
}
}
}
export { evalWorker as EvalWorker };

View File

@ -0,0 +1,405 @@
import { ApiResponse } from "api/ApiResponses";
import LibraryApi from "api/LibraryAPI";
import {
createMessage,
customJSLibraryMessages,
} from "@appsmith/constants/messages";
import {
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { Toaster, Variant } from "design-system";
import {
actionChannel,
ActionPattern,
all,
call,
put,
select,
take,
takeEvery,
takeLatest,
} from "redux-saga/effects";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
import { EVAL_WORKER_ACTIONS } from "workers/Evaluation/evalWorkerActions";
import { validateResponse } from "./ErrorSagas";
import { evaluateTreeSaga, EvalWorker } from "./EvaluationsSaga";
import log from "loglevel";
import { APP_MODE } from "entities/App";
import { getAppMode } from "selectors/applicationSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { TJSLibrary } from "workers/common/JSLibrary";
import { getUsedActionNames } from "selectors/actionSelectors";
import AppsmithConsole from "utils/AppsmithConsole";
import { selectInstalledLibraries } from "selectors/entitiesSelector";
export function parseErrorMessage(text: string) {
return text
.split(": ")
.slice(1)
.join("");
}
function* handleInstallationFailure(
url: string,
message: string,
accessor?: string[],
) {
if (accessor) {
yield call(
EvalWorker.request,
EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY,
accessor,
);
}
AppsmithConsole.error({
text: `Failed to install library script at ${url}`,
});
Toaster.show({
text: message || `Failed to install library script at ${url}`,
variant: Variant.danger,
});
yield put({
type: ReduxActionErrorTypes.INSTALL_LIBRARY_FAILED,
payload: { url, show: false },
});
AnalyticsUtil.logEvent("INSTALL_LIBRARY", { url, success: false });
}
export function* installLibrarySaga(lib: Partial<TJSLibrary>) {
const { url } = lib;
const takenNamesMap: Record<string, true> = yield select(
getUsedActionNames,
"",
);
const installedLibraries: TJSLibrary[] = yield select(
selectInstalledLibraries,
);
const takenAccessors = ([] as string[]).concat(
...installedLibraries.map((lib) => lib.accessor),
);
const { accessor, defs, error, success } = yield call(
EvalWorker.request,
EVAL_WORKER_ACTIONS.INSTALL_LIBRARY,
{
url,
takenNamesMap,
takenAccessors,
},
);
if (!success) {
log.debug("Failed to install locally");
yield call(handleInstallationFailure, url as string, error?.message);
return;
}
const name: string = lib.name || accessor[accessor.length - 1];
const applicationId: string = yield select(getCurrentApplicationId);
const versionMatch = (url as string).match(/(?:@)(\d+\.)(\d+\.)(\d+)/);
let [version = ""] = versionMatch ? versionMatch : [];
version = version.startsWith("@") ? version.slice(1) : version;
let stringifiedDefs = "";
try {
stringifiedDefs = JSON.stringify(defs);
} catch (e) {
stringifiedDefs = JSON.stringify({
"!name": `LIB/${accessor[accessor.length - 1]}`,
});
}
const response: ApiResponse<boolean> = yield call(
LibraryApi.addLibrary,
applicationId,
{
name,
version,
accessor,
defs: stringifiedDefs,
url,
},
);
try {
const isValidResponse: boolean = yield validateResponse(response, false);
if (!isValidResponse || !response.data) {
log.debug("Install API failed");
yield call(handleInstallationFailure, url as string, "", accessor);
return;
}
} catch (e) {
yield call(
handleInstallationFailure,
url as string,
(e as Error).message,
accessor,
);
return;
}
try {
CodemirrorTernService.updateDef(defs["!name"], defs);
AnalyticsUtil.logEvent("DEFINITIONS_GENERATION", { url, success: true });
} catch (e) {
Toaster.show({
text: createMessage(customJSLibraryMessages.AUTOCOMPLETE_FAILED, name),
variant: Variant.warning,
});
AppsmithConsole.warning({
text: `Failed to generate code definitions for ${name}`,
});
AnalyticsUtil.logEvent("DEFINITIONS_GENERATION", { url, success: false });
log.debug("Failed to update Tern defs", e);
}
yield put({
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
payload: {
libs: [
{
name,
version,
url,
accessor,
},
],
add: true,
},
});
//TODO: Check if we could avoid this.
yield call(evaluateTreeSaga, [], false, true, true);
yield put({
type: ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
payload: {
url,
accessor,
version,
name,
},
});
Toaster.show({
text: createMessage(
customJSLibraryMessages.INSTALLATION_SUCCESSFUL,
accessor[accessor.length - 1],
),
variant: Variant.success,
});
AnalyticsUtil.logEvent("INSTALL_LIBRARY", {
url,
namespace: accessor.join("."),
success: true,
});
AppsmithConsole.info({
text: `${name} installed successfully`,
});
}
function* uninstallLibrarySaga(action: ReduxAction<TJSLibrary>) {
const { accessor, name } = action.payload;
const applicationId: string = yield select(getCurrentApplicationId);
try {
const response: ApiResponse = yield call(
LibraryApi.removeLibrary,
applicationId,
action.payload,
);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
yield put({
type: ReduxActionErrorTypes.UNINSTALL_LIBRARY_FAILED,
payload: accessor,
});
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
url: action.payload.url,
success: false,
});
return;
}
yield put({
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
payload: {
libs: [action.payload],
add: false,
},
});
const { success }: { success: boolean } = yield call(
EvalWorker.request,
EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY,
accessor,
);
if (!success) {
Toaster.show({
text: createMessage(customJSLibraryMessages.UNINSTALL_FAILED, name),
variant: Variant.danger,
});
}
try {
CodemirrorTernService.removeDef(`LIB/${accessor[accessor.length - 1]}`);
} catch (e) {
log.debug(`Failed to remove definitions for ${name}`, e);
}
yield call(evaluateTreeSaga, [], false, true, true);
yield put({
type: ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS,
payload: action.payload,
});
Toaster.show({
text: createMessage(customJSLibraryMessages.UNINSTALL_SUCCESS, name),
variant: Variant.success,
});
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
url: action.payload.url,
success: true,
});
} catch (e) {
Toaster.show({
text: createMessage(customJSLibraryMessages.UNINSTALL_FAILED, name),
variant: Variant.danger,
});
AnalyticsUtil.logEvent("UNINSTALL_LIBRARY", {
url: action.payload.url,
success: false,
});
}
}
function* fetchJSLibraries(action: ReduxAction<string>) {
const applicationId: string = action.payload;
const mode: APP_MODE = yield select(getAppMode);
try {
const response: ApiResponse = yield call(
LibraryApi.getLibraries,
applicationId,
mode,
);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) return;
const libraries = response.data as Array<TJSLibrary & { defs: string }>;
const {
message,
success,
}: { success: boolean; message: string } = yield call(
EvalWorker.request,
EVAL_WORKER_ACTIONS.LOAD_LIBRARIES,
libraries.map((lib) => ({
name: lib.name,
version: lib.version,
url: lib.url,
accessor: lib.accessor,
})),
);
if (!success) {
if (mode === APP_MODE.EDIT) {
yield put({
type: ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS,
payload: libraries.map((lib) => ({
name: lib.name,
accessor: lib.accessor,
version: lib.version,
url: lib.url,
docsURL: lib.docsURL,
})),
});
const errorMessage = parseErrorMessage(message);
Toaster.show({
text: errorMessage,
variant: Variant.warning,
});
} else {
yield put({
type: ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
});
}
return;
}
if (mode === APP_MODE.EDIT) {
for (const lib of libraries) {
try {
const defs = JSON.parse(lib.defs);
CodemirrorTernService.updateDef(defs["!name"], defs);
} catch (e) {
Toaster.show({
text: createMessage(
customJSLibraryMessages.AUTOCOMPLETE_FAILED,
lib.name,
),
variant: Variant.info,
});
}
}
yield put({
type: ReduxActionTypes.UPDATE_LINT_GLOBALS,
payload: {
libs: libraries,
},
});
}
yield put({
type: ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS,
payload: libraries.map((lib) => ({
name: lib.name,
accessor: lib.accessor,
version: lib.version,
url: lib.url,
docsURL: lib.docsURL,
})),
});
} catch (e) {
yield put({
type: ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
});
}
}
function* startInstallationRequestChannel() {
const queueInstallChannel: ActionPattern<any> = yield actionChannel([
ReduxActionTypes.INSTALL_LIBRARY_INIT,
]);
while (true) {
const action: ReduxAction<Partial<TJSLibrary>> = yield take(
queueInstallChannel,
);
yield put({
type: ReduxActionTypes.INSTALL_LIBRARY_START,
payload: action.payload.url,
});
yield call(installLibrarySaga, action.payload);
}
}
export default function*() {
yield all([
takeEvery(ReduxActionTypes.UNINSTALL_LIBRARY_INIT, uninstallLibrarySaga),
takeLatest(ReduxActionTypes.FETCH_JS_LIBRARIES_INIT, fetchJSLibraries),
call(startInstallationRequestChannel),
]);
}

View File

@ -1,8 +1,13 @@
import { setLintingErrors } from "actions/lintingActions";
import {
ReduxAction,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { APP_MODE } from "entities/App";
import { call, put, select } from "redux-saga/effects";
import { call, put, select, takeEvery } from "redux-saga/effects";
import { getAppMode } from "selectors/entitiesSelector";
import { GracefulWorkerService } from "utils/WorkerUtil";
import { TJSLibrary } from "workers/common/JSLibrary";
import {
LintTreeRequest,
LintTreeResponse,
@ -18,10 +23,19 @@ export const lintWorker = new GracefulWorkerService(
}),
);
export function* lintTreeSaga({
pathsToLint,
unevalTree,
}: LintTreeSagaRequestData) {
function* updateLintGlobals(action: ReduxAction<TJSLibrary>) {
const appMode: APP_MODE = yield select(getAppMode);
const isEditorMode = appMode === APP_MODE.EDIT;
if (!isEditorMode) return;
yield call(
lintWorker.request,
LINT_WORKER_ACTIONS.UPDATE_LINT_GLOBALS,
action.payload,
);
}
export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
const { pathsToLint, unevalTree } = action.payload;
// only perform lint operations in edit mode
const appMode: APP_MODE = yield select(getAppMode);
if (appMode !== APP_MODE.EDIT) return;
@ -40,3 +54,8 @@ export function* lintTreeSaga({
yield put(setLintingErrors(errors));
yield call(logLatestLintPropertyErrors, { errors, dataTree: unevalTree });
}
export default function* lintTreeSagaWatcher() {
yield takeEvery(ReduxActionTypes.UPDATE_LINT_GLOBALS, updateLintGlobals);
yield takeEvery(ReduxActionTypes.LINT_TREE, lintTreeSaga);
}

View File

@ -69,13 +69,7 @@ import {
} from "utils/helpers";
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
import { checkIfMigrationIsNeeded } from "utils/DSLMigrations";
import {
getAllPageIds,
getEditorConfigs,
getExistingPageNames,
getWidgets,
} from "./selectors";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getAllPageIds, getEditorConfigs, getWidgets } from "./selectors";
import { IncorrectBindingError, validateResponse } from "./ErrorSagas";
import { ApiResponse } from "api/ApiResponses";
import {
@ -124,7 +118,6 @@ import {
import WidgetFactory from "utils/WidgetFactory";
import { toggleShowDeviationDialog } from "actions/onboardingActions";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { builderURL } from "RouteBuilder";
import { failFastApiCalls } from "./InitSagas";
import { hasManagePagePermission } from "@appsmith/utils/permissionHelpers";
@ -133,6 +126,7 @@ import { getSelectedWidgets } from "selectors/ui";
import { checkAndLogErrorsIfCyclicDependency } from "./helper";
import { LOCAL_STORAGE_KEYS } from "utils/localStorage";
import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions";
import { getUsedActionNames } from "selectors/actionSelectors";
import { getPageList } from "selectors/entitiesSelector";
const WidgetTypes = WidgetFactory.widgetTypes;
@ -788,10 +782,10 @@ export function* updateWidgetNameSaga(
try {
const { widgetName } = yield select(getWidgetName, action.payload.id);
const layoutId: string | undefined = yield select(getCurrentLayoutId);
const evalTree: DataTree = yield select(getDataTree);
const pageId: string | undefined = yield select(getCurrentPageId);
const existingPageNames: Record<string, unknown> = yield select(
getExistingPageNames,
const getUsedNames: Record<string, true> = yield select(
getUsedActionNames,
"",
);
// TODO(abhinav): Why do we need to jump through these hoops just to
@ -868,12 +862,7 @@ export function* updateWidgetNameSaga(
} else {
// check if name is not conflicting with any
// existing entity/api/queries/reserved words
if (
isNameValid(action.payload.newName, {
...evalTree,
...existingPageNames,
})
) {
if (isNameValid(action.payload.newName, getUsedNames)) {
const request: UpdateWidgetNameRequest = {
newName: action.payload.newName,
oldName: widgetName,

View File

@ -2,8 +2,12 @@ import { DataTree } from "entities/DataTree/dataTreeFactory";
import { createSelector } from "reselect";
import WidgetFactory from "utils/WidgetFactory";
import { FlattenedWidgetProps } from "widgets/constants";
import { TJSLibrary } from "workers/common/JSLibrary";
import { getDataTree } from "./dataTreeSelectors";
import { getExistingPageNames } from "./entitiesSelector";
import {
getExistingPageNames,
selectInstalledLibraries,
} from "./entitiesSelector";
import {
getErrorForApiName,
getErrorForJSObjectName,
@ -19,10 +23,12 @@ export const getUsedActionNames = createSelector(
getExistingPageNames,
getDataTree,
getParentWidget,
selectInstalledLibraries,
(
pageNames: Record<string, any>,
dataTree: DataTree,
parentWidget: FlattenedWidgetProps | undefined,
installedLibraries: TJSLibrary[],
) => {
const map: Record<string, boolean> = {};
// The logic has been copied from Explorer/Entity/Name.tsx Component.
@ -41,6 +47,12 @@ export const getUsedActionNames = createSelector(
Object.keys(dataTree).forEach((treeItem: string) => {
map[treeItem] = true;
});
const libAccessors = ([] as string[]).concat(
...installedLibraries.map((lib) => lib.accessor),
);
for (const accessor of libAccessors) {
map[accessor] = true;
}
}
return map;

View File

@ -29,6 +29,9 @@ import {
EVAL_ERROR_PATH,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { InstallState } from "reducers/uiReducers/libraryReducer";
import recommendedLibraries from "pages/Editor/Explorer/Libraries/recommendedLibraries";
import { TJSLibrary } from "workers/common/JSLibrary";
export const getEntities = (state: AppState): AppState["entities"] =>
state.entities;
@ -848,3 +851,51 @@ export const getNumberOfEntitiesInCurrentPage = createSelector(
);
},
);
export const selectIsInstallerOpen = (state: AppState) =>
state.ui.libraries.isInstallerOpen;
export const selectInstallationStatus = (state: AppState) =>
state.ui.libraries.installationStatus;
export const selectInstalledLibraries = (state: AppState) =>
state.ui.libraries.installedLibraries;
export const selectStatusForURL = (url: string) =>
createSelector(selectInstallationStatus, (statusMap) => {
return statusMap[url];
});
export const selectIsLibraryInstalled = createSelector(
[selectInstalledLibraries, (_: AppState, url: string) => url],
(installedLibraries, url) => {
return !!installedLibraries.find((lib) => lib.url === url);
},
);
export const selectQueuedLibraries = createSelector(
selectInstallationStatus,
(statusMap) => {
return Object.keys(statusMap).filter(
(url) => statusMap[url] === InstallState.Queued,
);
},
);
export const selectLibrariesForExplorer = createSelector(
selectInstalledLibraries,
selectInstallationStatus,
(libs, libStatus) => {
const queuedInstalls = Object.keys(libStatus)
.filter((key) => libStatus[key] === InstallState.Queued)
.map((url) => {
const recommendedLibrary = recommendedLibraries.find(
(lib) => lib.url === url,
);
return {
name: recommendedLibrary?.name || url,
docsURL: recommendedLibrary?.url || url,
version: recommendedLibrary?.version || "",
url: recommendedLibrary?.url || url,
accessor: [],
} as TJSLibrary;
});
return [...queuedInstalls, ...libs];
},
);

View File

@ -275,7 +275,14 @@ export type EventName =
| "BRANDING_PROPERTY_UPDATE"
| "BRANDING_SUBMIT_CLICK"
| "Cmd+Click Navigation"
| "WIDGET_PROPERTY_SEARCH";
| "WIDGET_PROPERTY_SEARCH"
| LIBRARY_EVENTS;
export type LIBRARY_EVENTS =
| "INSTALL_LIBRARY"
| "DEFINITIONS_GENERATION"
| "UNINSTALL_LIBRARY"
| "EDIT_LIBRARY_URL";
export type AUDIT_LOGS_EVENT_NAMES =
| "AUDIT_LOGS_CLEAR_FILTERS"

View File

@ -1,17 +1,13 @@
import _, { get, isString, VERSION as lodashVersion } from "lodash";
import _, { get, isString } from "lodash";
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import { Action } from "entities/Action";
import moment from "moment-timezone";
import { WidgetProps } from "widgets/BaseWidget";
import parser from "fast-xml-parser";
import { Severity } from "entities/AppsmithConsole";
import {
getEntityNameAndPropertyPath,
isJSAction,
isTrueObject,
} from "workers/Evaluation/evaluationUtils";
import forge from "node-forge";
import { DataTreeEntity } from "entities/DataTree/dataTreeFactory";
import { getType, Types } from "./TypeHelpers";
import { ViewTypes } from "components/formControls/utils";
@ -130,76 +126,6 @@ export type EvalError = {
context?: Record<string, any>;
};
export enum EVAL_WORKER_ACTIONS {
SETUP = "SETUP",
EVAL_TREE = "EVAL_TREE",
EVAL_ACTION_BINDINGS = "EVAL_ACTION_BINDINGS",
EVAL_TRIGGER = "EVAL_TRIGGER",
PROCESS_TRIGGER = "PROCESS_TRIGGER",
CLEAR_CACHE = "CLEAR_CACHE",
VALIDATE_PROPERTY = "VALIDATE_PROPERTY",
UNDO = "undo",
REDO = "redo",
EVAL_EXPRESSION = "EVAL_EXPRESSION",
UPDATE_REPLAY_OBJECT = "UPDATE_REPLAY_OBJECT",
SET_EVALUATION_VERSION = "SET_EVALUATION_VERSION",
INIT_FORM_EVAL = "INIT_FORM_EVAL",
EXECUTE_SYNC_JS = "EXECUTE_SYNC_JS",
LINT_TREE = "LINT_TREE",
}
export type ExtraLibrary = {
version: string;
docsURL: string;
displayName: string;
accessor: string;
lib: any;
};
export const extraLibraries: ExtraLibrary[] = [
{
accessor: "_",
lib: _,
version: lodashVersion,
docsURL: `https://lodash.com/docs/${lodashVersion}`,
displayName: "lodash",
},
{
accessor: "moment",
lib: moment,
version: moment.version,
docsURL: `https://momentjs.com/docs/`,
displayName: "moment",
},
{
accessor: "xmlParser",
lib: parser,
version: "3.17.5",
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
displayName: "xmlParser",
},
{
accessor: "forge",
// We are removing some functionalities of node-forge because they wont
// work in the worker thread
lib: _.omit(forge, ["tls", "http", "xhr", "socket", "task"]),
version: "1.3.0",
docsURL: "https://github.com/digitalbazaar/forge",
displayName: "forge",
},
];
/**
* creates dynamic list of constants based on
* current list of extra libraries i.e lodash("_"), moment etc
* to be used in widget and entity name validations
*/
export const extraLibrariesNames = extraLibraries.reduce(
(prev: Record<string, string>, curr) => {
prev[curr.accessor] = curr.accessor;
return prev;
},
{},
);
export interface DynamicPath {
key: string;
value?: string;
@ -319,7 +245,6 @@ export const unsafeFunctionForEval = [
"setInterval",
"clearInterval",
"setImmediate",
"importScripts",
"Navigator",
];

View File

@ -0,0 +1,47 @@
/**
* This file contains the utility function to send and receive messages from the worker.
* TRequestMessage<TBody> is used to send a request to/from the worker.
* TResponseMessage<TBody> is used to send a response to/from the worker.
* TDefaultMessage<TBody> is used to send a message to/from worker. Does not expect a response.
*/
export enum MessageType {
REQUEST = "REQUEST",
RESPONSE = "RESPONSE",
DEFAULT = "DEFAULT",
}
type TRequestMessage<TBody> = {
body: TBody;
messageId: string;
messageType: MessageType.REQUEST;
};
type TResponseMessage<TBody> = {
body: TBody;
messageId: string;
messageType: MessageType.RESPONSE;
};
type TDefaultMessage<TBody> = {
body: TBody;
messageType: MessageType.DEFAULT;
};
export type TMessage<TBody> =
| TRequestMessage<TBody>
| TResponseMessage<TBody>
| TDefaultMessage<TBody>;
/** Avoid from using postMessage directly.
* This function should be used to send messages to the worker and back.
* Purpose: To have some standardization in the messages that are transferred.
* TODO: Add support for window postMessage options
* TODO: Add support for transferable objects.
*/
export function sendMessage(
this: Worker | typeof globalThis,
message: TMessage<unknown>,
) {
this.postMessage(message);
}

View File

@ -56,8 +56,9 @@ class MockWorkerClass implements WorkerClass {
this.messages.push(message);
const counter = setTimeout(() => {
const response = {
requestId: message.requestId,
responseData: message.requestData,
messageId: message.messageId,
messageType: "RESPONSE",
body: { data: message.body.data },
};
this.sendEvent({ data: response });
this.responses.delete(counter);
@ -218,65 +219,4 @@ describe("GracefulWorkerService", () => {
await shutdown.toPromise();
expect(await task.toPromise()).not.toEqual(message);
});
test("duplex request starter", async () => {
const MockWorker = new MockWorkerClass();
const w = new GracefulWorkerService(MockWorker);
await runSaga({}, w.start);
// Need this to work with eslint
if (MockWorker.instance === undefined) {
expect(MockWorker.instance).toBeDefined();
return;
}
const requestData = { message: "Hello" };
const method = "duplex_test";
MockWorker.instance.postMessage = jest.fn();
const duplexRequest = await runSaga(
{},
w.duplexRequest,
method,
requestData,
);
const handlers = await duplexRequest.toPromise();
expect(handlers).toHaveProperty("isFinishedChannel");
expect(MockWorker.instance.postMessage).toBeCalledWith({
method,
requestData,
requestId: expect.stringContaining(method),
});
});
test("duplex response channel handler", async () => {
const MockWorker = new MockWorkerClass();
const w = new GracefulWorkerService(MockWorker);
await runSaga({}, w.start);
// Need this to work with eslint
if (MockWorker.instance === undefined) {
expect(MockWorker.instance).toBeDefined();
return;
}
const mainThreadResponseChannel = channel();
const workerRequestId = "testID";
runSaga(
{},
// @ts-expect-error: type mismatch
w.duplexResponseHandler,
mainThreadResponseChannel,
);
MockWorker.instance.postMessage = jest.fn();
let randomRequestCount = Math.floor(Math.random() * 10);
for (randomRequestCount; randomRequestCount > 0; randomRequestCount--) {
mainThreadResponseChannel.put({
test: randomRequestCount,
requestId: workerRequestId,
});
expect(MockWorker.instance.postMessage).toBeCalledWith({
test: randomRequestCount,
requestId: workerRequestId,
});
}
});
});

View File

@ -1,8 +1,9 @@
import { cancelled, delay, put, spawn, take } from "redux-saga/effects";
import { cancelled, delay, put, take } from "redux-saga/effects";
import { channel, Channel, buffers } from "redux-saga";
import { uniqueId } from "lodash";
import log from "loglevel";
// import { executeDynamicTriggerRequest } from "sagas/EvaluationsSaga";
import { TMessage, MessageType, sendMessage } from "./MessageUtil";
/**
* Wrap a webworker to provide a synchronous request-response semantic.
*
@ -30,7 +31,6 @@ import log from "loglevel";
* Note: The worker will hold ALL requests, even in case of restarts.
* If we do not want that behaviour, we should create a new GracefulWorkerService.
*/
// TODO: Add a compatible listener layer on the worker to complete the framework.
// TODO: Extract the worker wrapper into a library to be useful to anyone with WebWorkers + redux-saga.
// TODO: Add support for timeouts on requests and shutdown.
// TODO: Add a readiness + liveness probes.
@ -50,24 +50,21 @@ export class GracefulWorkerService {
private readonly _workerClass: Worker;
public mainThreadRequestChannel: Channel<any>;
public mainThreadResponseChannel: Channel<any>;
private listenerChannel: Channel<TMessage<any>>;
constructor(workerClass: Worker) {
this.shutdown = this.shutdown.bind(this);
this.start = this.start.bind(this);
this.request = this.request.bind(this);
this._broker = this._broker.bind(this);
this.duplexRequest = this.duplexRequest.bind(this);
this.duplexResponseHandler = this.duplexResponseHandler.bind(this);
this.request = this.request.bind(this);
this.respond = this.respond.bind(this);
// Do not buffer messages on this channel
this._readyChan = channel(buffers.none());
this._isReady = false;
this._channels = new Map<string, Channel<any>>();
this._workerClass = workerClass;
this.mainThreadRequestChannel = channel();
this.mainThreadResponseChannel = channel();
this.listenerChannel = channel();
}
/**
@ -81,10 +78,7 @@ export class GracefulWorkerService {
// Inform all pending requests that we're good to go!
this._isReady = true;
yield put(this._readyChan, true);
yield spawn(this.duplexResponseHandler, this.mainThreadResponseChannel);
return {
mainThreadRequestChannel: this.mainThreadRequestChannel,
};
return this.listenerChannel;
}
/**
@ -104,8 +98,7 @@ export class GracefulWorkerService {
this._Worker.removeEventListener("message", this._broker);
this._Worker.terminate();
this._Worker = undefined;
this.mainThreadRequestChannel.close();
this.mainThreadResponseChannel.close();
this.listenerChannel.close();
}
/**
@ -120,6 +113,20 @@ export class GracefulWorkerService {
return false;
}
*respond(messageId = "", data = {}): any {
if (!messageId) return;
yield this.ready(true);
if (!this._Worker) return;
const messageType = MessageType.RESPONSE;
sendMessage.call(this._Worker, {
body: {
data,
},
messageId,
messageType,
});
}
/**
* Send a request to the worker for processing.
* If the worker isn't ready, we wait for it to become ready.
@ -129,7 +136,7 @@ export class GracefulWorkerService {
*
* @returns response from the worker
*/
*request(method: string, requestData = {}): any {
*request(method: string, data = {}): any {
yield this.ready(true);
// Impossible case, but helps avoid `?` later in code and makes it clearer.
if (!this._Worker) return;
@ -137,22 +144,25 @@ export class GracefulWorkerService {
/**
* We create a unique channel to wait for a response of this specific request.
*/
const requestId = `${method}__${uniqueId()}`;
const messageId = `${method}__${uniqueId()}`;
const ch = channel();
this._channels.set(requestId, ch);
this._channels.set(messageId, ch);
const mainThreadStartTime = performance.now();
let timeTaken;
try {
this._Worker.postMessage({
method,
requestData,
requestId,
sendMessage.call(this._Worker, {
messageType: MessageType.REQUEST,
body: {
method,
data,
},
messageId,
});
// The `this._broker` method is listening to events and will pass response to us over this channel.
const response = yield take(ch);
timeTaken = response.timeTaken;
const { responseData } = response;
const { data: responseData } = response;
return responseData;
} finally {
// Log perf of main thread and worker
@ -173,86 +183,23 @@ export class GracefulWorkerService {
}
// Cleanup
ch.close();
this._channels.delete(requestId);
this._channels.delete(messageId);
}
}
/**
* When there needs to be a back and forth between both the threads,
* you can use duplex request to avoid closing a channel
* */
*duplexRequest(method: string, requestData = {}): any {
yield this.ready(false);
// Impossible case, but helps avoid `?` later in code and makes it clearer.
if (!this._Worker) return;
/**
* We create a unique channel to wait for a response of this specific request.
*/
const workerRequestId = `${method}__${uniqueId()}`;
// The worker channel is the main channel
// where the web worker messages will get posted
const isFinishedChannel = channel();
this._channels.set(workerRequestId, isFinishedChannel);
// And post the first message to the worker
this._Worker.postMessage({
method,
requestData,
requestId: workerRequestId,
});
// Returning these channels to the main thread so that they can listen and post on it
return {
isFinishedChannel: isFinishedChannel,
};
}
*duplexResponseHandler(mainThreadResponseChannel: Channel<any>) {
if (!this._Worker) return;
try {
const keepAlive = true;
while (keepAlive) {
// Wait for the main thread to respond back after a request
const response: { finished: unknown; requestId: string } = yield take(
mainThreadResponseChannel,
);
// send response to worker
this._Worker.postMessage({
...response,
requestId: response.requestId,
});
}
} catch (e) {
log.error(e);
}
}
private _broker(event: MessageEvent) {
if (!event || !event.data) {
return;
}
const { promisified, requestId, responseData, timeTaken } = event.data;
const ch = this._channels.get(requestId);
// Channel could have been deleted if the request gets cancelled before the WebWorker can respond.
// In that case, we want to drop the request.
if (promisified) {
if (responseData.finished) {
if (ch) {
ch.put({ requestData: responseData, timeTaken, requestId });
this._channels.delete(requestId);
}
} else {
this.mainThreadRequestChannel.put({
requestData: responseData,
timeTaken,
requestId,
mainThreadResponseChannel: this.mainThreadResponseChannel,
});
private _broker(event: MessageEvent<TMessage<any>>) {
if (!event || !event.data) return;
const { body, messageType } = event.data;
if (messageType === MessageType.RESPONSE) {
const { messageId } = event.data;
if (!messageId) return;
const ch = this._channels.get(messageId);
if (ch) {
ch.put(body);
this._channels.delete(messageId);
}
} else {
if (ch) {
ch.put({ responseData, timeTaken, requestId });
}
this.listenerChannel.put(event.data);
}
}
}

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// Heavily inspired from https://github.com/codemirror/CodeMirror/blob/master/addon/tern/tern.js
import tern, { Server, Def } from "tern";
import { Server, Def } from "tern";
import ecma from "constants/defs/ecmascript.json";
import lodash from "constants/defs/lodash.json";
import base64 from "constants/defs/base64-js.json";
@ -147,13 +147,10 @@ class CodeMirrorTernService {
this.server = new TernWorkerServer(this);
}
resetServer() {
this.server = new tern.Server({
async: true,
defs: DEFS,
});
resetServer = () => {
this.server = new TernWorkerServer(this);
this.docs = Object.create(null);
}
};
complete(cm: CodeMirror.Editor) {
cm.showHint({
@ -247,7 +244,7 @@ class CodeMirrorTernService {
const dataType = getDataType(completion.type);
if (data.guess) className += " " + cls + "guess";
let completionText = completion.name + after;
if (dataType === "FUNCTION") {
if (dataType === "FUNCTION" && !completion.origin?.startsWith("LIB/")) {
completionText = completionText + "()";
}
const codeMirrorCompletion: Completion = {

View File

@ -575,12 +575,9 @@ describe("isNameValid()", () => {
it("works properly", () => {
const invalidEntityNames = [
"console",
"moment",
"Promise",
"appsmith",
"Math",
"_",
"forge",
"yield",
"Boolean",
"ReferenceError",

View File

@ -13,7 +13,7 @@ import { get, set, isNil, has, uniq } from "lodash";
import { Workspace } from "@appsmith/constants/workspaceConstants";
import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers";
import moment from "moment";
import { extraLibrariesNames, isDynamicValue } from "./DynamicBindingUtils";
import { isDynamicValue } from "./DynamicBindingUtils";
import { ApiResponse } from "api/ApiResponses";
import { DSLWidget } from "widgets/constants";
import * as Sentry from "@sentry/react";
@ -412,7 +412,6 @@ export const isNameValid = (
has(DATA_TREE_KEYWORDS, name) ||
has(DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS, name) ||
has(APPSMITH_GLOBAL_FUNCTIONS, name) ||
has(extraLibrariesNames, name) ||
has(invalidNames, name)
);
};

View File

@ -300,7 +300,6 @@ export const DATA_TREE_FUNCTIONS: Record<
export const enhanceDataTreeWithFunctions = (
dataTree: Readonly<DataTree>,
requestId = "",
// Whether not to add functions like "run", "clear" to entity
skipEntityFunctions = false,
eventType?: EventType,
@ -324,7 +323,6 @@ export const enhanceDataTreeWithFunctions = (
pusher.bind(
{
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
REQUEST_ID: requestId,
EVENT_TYPE: eventType,
},
func,
@ -339,7 +337,6 @@ export const enhanceDataTreeWithFunctions = (
pusher.bind(
{
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
REQUEST_ID: requestId,
},
funcOrFuncCreator,
),
@ -367,7 +364,6 @@ export const enhanceDataTreeWithFunctions = (
export const pusher = function(
this: {
TRIGGER_COLLECTOR: ActionDescription[];
REQUEST_ID: string;
EVENT_TYPE?: EventType;
},
action: ActionDispatcherWithExecutionType,
@ -383,6 +379,6 @@ export const pusher = function(
if (executionType && executionType === ExecutionType.TRIGGER) {
this.TRIGGER_COLLECTOR.push(actionPayload);
} else {
return promisifyAction(this.REQUEST_ID, actionPayload, this.EVENT_TYPE);
return promisifyAction(actionPayload, this.EVENT_TYPE);
}
};

View File

@ -1,4 +1,4 @@
import { createGlobalData, EvalResult } from "workers/Evaluation/evaluate";
import { createGlobalData } from "workers/Evaluation/evaluate";
const ctx: Worker = self as any;
/*
@ -8,17 +8,14 @@ const ctx: Worker = self as any;
*
* needs a REQUEST_ID to be passed in to know which request is going on right now
*/
import { EVAL_WORKER_ACTIONS } from "utils/DynamicBindingUtils";
import {
ActionDescription,
ActionTriggerType,
} from "entities/DataTree/actionTriggers";
import { ActionDescription } from "entities/DataTree/actionTriggers";
import _ from "lodash";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { dataTreeEvaluator } from "workers/Evaluation/evaluation.worker";
import { dataTreeEvaluator } from "./handlers/evalTree";
import { TMessage, sendMessage, MessageType } from "utils/MessageUtil";
import { MAIN_THREAD_ACTION } from "./evalWorkerActions";
export const promisifyAction = (
workerRequestId: string,
actionDescription: ActionDescription,
eventType?: EventType,
) => {
@ -31,34 +28,32 @@ export const promisifyAction = (
self.IS_ASYNC = true;
throw new Error("Async function called in a sync field");
}
const workerRequestIdCopy = workerRequestId.concat("");
return new Promise((resolve, reject) => {
// We create a new sub request id for each request going on so that we can resolve the correct one later on
const subRequestId = _.uniqueId(`${workerRequestIdCopy}_`);
const messageId = _.uniqueId(`${actionDescription.type}_`);
// send an execution request to the main thread
const responseData = {
const data = {
trigger: actionDescription,
errors: [],
subRequestId,
eventType,
};
ctx.postMessage({
type: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
responseData,
requestId: workerRequestIdCopy,
promisified: true,
sendMessage.call(ctx, {
messageId,
messageType: MessageType.REQUEST,
body: {
method: MAIN_THREAD_ACTION.PROCESS_TRIGGER,
data,
},
});
const processResponse = function(event: MessageEvent) {
const { data, eventType, method, requestId, success } = event.data;
const processResponse = function(event: MessageEvent<TMessage<any>>) {
const { messageType } = event.data;
if (messageType !== MessageType.RESPONSE) return;
const { body, messageId: resMessageId } = event.data;
const { data: messageData } = body;
const { data, eventType, success } = messageData;
// This listener will get all the messages that come to the worker
// we need to find the correct one pertaining to this promise
if (
method === EVAL_WORKER_ACTIONS.PROCESS_TRIGGER &&
requestId === workerRequestIdCopy &&
subRequestId === event.data.data.subRequestId
) {
if (resMessageId === messageId && messageType === MessageType.RESPONSE) {
// If we get a response for this same promise we will resolve or reject it
// We could not find a data tree evaluator,
// maybe the page changed, or we have a cyclical dependency
if (!dataTreeEvaluator) {
@ -71,7 +66,6 @@ export const promisifyAction = (
resolvedFunctions: dataTreeEvaluator.resolvedFunctions,
isTriggerBased: true,
context: {
requestId: workerRequestId,
eventType,
},
});
@ -94,31 +88,3 @@ export const promisifyAction = (
ctx.addEventListener("message", processResponse);
});
};
// To indicate the main thread that the processing of the trigger is done
// we send a finished message
export const completePromise = (requestId: string, result: EvalResult) => {
ctx.postMessage({
type: EVAL_WORKER_ACTIONS.PROCESS_TRIGGER,
responseData: {
finished: true,
result,
},
requestId,
promisified: true,
});
};
export const confirmationPromise = function(
requestId: string,
func: any,
name: string,
...args: any[]
) {
const payload: ActionDescription = {
type: ActionTriggerType.CONFIRMATION_MODAL,
payload: {
funName: name,
},
};
return promisifyAction(requestId, payload).then(() => func(...args));
};

View File

@ -0,0 +1,16 @@
//@ts-expect-error no types.
import * as documentMock from "linkedom/worker";
export const DOM_APIS = Object.keys(documentMock).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, true>);
export default function() {
for (const [key, value] of Object.entries(documentMock)) {
//@ts-expect-error no types
self[key] = value;
}
const dom = documentMock.parseHTML(`<!DOCTYPE html><body></body>`);
self.document = dom.window.document;
}

View File

@ -1,5 +1,5 @@
import { createGlobalData } from "./evaluate";
import { dataTreeEvaluator } from "./evaluation.worker";
import { dataTreeEvaluator } from "./handlers/evalTree";
export const _internalSetTimeout = self.setTimeout;
export const _internalClearTimeout = self.clearTimeout;

View File

@ -4,6 +4,8 @@ import { LogObject, Methods, Severity } from "entities/AppsmithConsole";
import { klona } from "klona/lite";
import moment from "moment";
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
import { sendMessage, MessageType } from "utils/MessageUtil";
import { MAIN_THREAD_ACTION } from "./evalWorkerActions";
import { _internalClearTimeout, _internalSetTimeout } from "./TimeoutOverride";
class UserLog {
@ -11,13 +13,11 @@ class UserLog {
private logs: LogObject[] = [];
private flushLogTimerId: number | undefined;
private requestInfo: {
requestId?: string;
eventType?: EventType;
triggerMeta?: TriggerMeta;
} | null = null;
public setCurrentRequestInfo(requestInfo: {
requestId?: string;
eventType?: EventType;
triggerMeta?: TriggerMeta;
}) {
@ -28,14 +28,16 @@ class UserLog {
if (this.flushLogTimerId) _internalClearTimeout(this.flushLogTimerId);
this.flushLogTimerId = _internalSetTimeout(() => {
const logs = this.flushLogs();
self.postMessage({
promisified: true,
responseData: {
logs,
eventType: this.requestInfo?.eventType,
triggerMeta: this.requestInfo?.triggerMeta,
sendMessage.call(self, {
messageType: MessageType.DEFAULT,
body: {
data: {
logs,
eventType: this.requestInfo?.eventType,
triggerMeta: this.requestInfo?.triggerMeta,
},
method: MAIN_THREAD_ACTION.PROCESS_LOGS,
},
requestId: this.requestInfo?.requestId,
});
}, this.flushLogsTimerDelay);
}

View File

@ -2,6 +2,7 @@ import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { PluginType } from "entities/Action";
import { createGlobalData } from "workers/Evaluation/evaluate";
import uniqueId from "lodash/uniqueId";
import { MessageType } from "utils/MessageUtil";
jest.mock("lodash/uniqueId");
describe("Add functions", () => {
@ -39,6 +40,12 @@ describe("Add functions", () => {
},
});
const messageCreator = (type: string, body: unknown) => ({
messageId: expect.stringContaining(type),
messageType: MessageType.REQUEST,
body,
});
beforeEach(() => {
workerEventMock.mockReset();
self.postMessage = workerEventMock;
@ -119,43 +126,39 @@ describe("Add functions", () => {
expect(
dataTreeWithFunctions.action1.run(null, null, actionParams),
).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
data: {
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
},
},
},
},
});
method: "PROCESS_TRIGGER",
}),
);
// Old syntax works with undefined values is treated as new syntax
expect(
dataTreeWithFunctions.action1.run(undefined, undefined, actionParams),
).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
data: {
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
},
},
},
},
});
method: "PROCESS_TRIGGER",
}),
);
// new syntax works
expect(
@ -164,60 +167,55 @@ describe("Add functions", () => {
.then(onSuccess)
.catch(onError),
).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
data: {
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: { param1: "value1" },
},
},
},
},
});
method: "PROCESS_TRIGGER",
}),
);
// New syntax without params
expect(dataTreeWithFunctions.action1.run()).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: {},
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
data: {
trigger: {
type: "RUN_PLUGIN_ACTION",
payload: {
actionId: "123",
params: {},
},
},
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("action.clear works", () => {
expect(dataTreeWithFunctions.action1.clear()).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "CLEAR_PLUGIN_ACTION",
payload: {
actionId: "123",
expect(workerEventMock).lastCalledWith(
messageCreator("CLEAR_PLUGIN_ACTION", {
data: {
trigger: {
type: "CLEAR_PLUGIN_ACTION",
payload: {
actionId: "123",
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("navigateTo works", () => {
@ -228,87 +226,82 @@ describe("Add functions", () => {
expect(
dataTreeWithFunctions.navigateTo(pageNameOrUrl, params, target),
).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "NAVIGATE_TO",
payload: {
pageNameOrUrl,
params,
target,
expect(workerEventMock).lastCalledWith(
messageCreator("NAVIGATE_TO", {
data: {
trigger: {
type: "NAVIGATE_TO",
payload: {
pageNameOrUrl,
params,
target,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("showAlert works", () => {
const message = "Alert message";
const style = "info";
expect(dataTreeWithFunctions.showAlert(message, style)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "SHOW_ALERT",
payload: {
message,
style,
expect(workerEventMock).lastCalledWith(
messageCreator("SHOW_ALERT", {
data: {
trigger: {
type: "SHOW_ALERT",
payload: {
message,
style,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("showModal works", () => {
const modalName = "Modal 1";
expect(dataTreeWithFunctions.showModal(modalName)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "SHOW_MODAL_BY_NAME",
payload: {
modalName,
expect(workerEventMock).lastCalledWith(
messageCreator("SHOW_MODAL_BY_NAME", {
data: {
trigger: {
type: "SHOW_MODAL_BY_NAME",
payload: {
modalName,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("closeModal works", () => {
const modalName = "Modal 1";
expect(dataTreeWithFunctions.closeModal(modalName)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "CLOSE_MODAL",
payload: {
modalName,
expect(workerEventMock).lastCalledWith(
messageCreator("CLOSE_MODAL", {
data: {
trigger: {
type: "CLOSE_MODAL",
payload: {
modalName,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("storeValue works", () => {
@ -323,62 +316,58 @@ describe("Add functions", () => {
expect(dataTreeWithFunctions.storeValue(key, value, persist)).resolves.toBe(
{},
);
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "STORE_VALUE",
payload: {
key,
value,
persist,
uniqueActionRequestId,
expect(workerEventMock).lastCalledWith(
messageCreator("STORE_VALUE", {
data: {
trigger: {
type: "STORE_VALUE",
payload: {
key,
value,
persist,
uniqueActionRequestId,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("removeValue works", () => {
const key = "some";
expect(dataTreeWithFunctions.removeValue(key)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "REMOVE_VALUE",
payload: {
key,
expect(workerEventMock).lastCalledWith(
messageCreator("REMOVE_VALUE", {
data: {
trigger: {
type: "REMOVE_VALUE",
payload: {
key,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("clearStore works", () => {
expect(dataTreeWithFunctions.clearStore()).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "CLEAR_STORE",
payload: null,
expect(workerEventMock).lastCalledWith(
messageCreator("CLEAR_STORE", {
data: {
trigger: {
type: "CLEAR_STORE",
payload: null,
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("download works", () => {
@ -387,44 +376,42 @@ describe("Add functions", () => {
const type = "text";
expect(dataTreeWithFunctions.download(data, name, type)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "DOWNLOAD",
payload: {
data,
name,
type,
expect(workerEventMock).lastCalledWith(
messageCreator("DOWNLOAD", {
data: {
trigger: {
type: "DOWNLOAD",
payload: {
data,
name,
type,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("copyToClipboard works", () => {
const data = "file";
expect(dataTreeWithFunctions.copyToClipboard(data)).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "COPY_TO_CLIPBOARD",
payload: {
data,
options: { debug: undefined, format: undefined },
expect(workerEventMock).lastCalledWith(
messageCreator("COPY_TO_CLIPBOARD", {
data: {
trigger: {
type: "COPY_TO_CLIPBOARD",
payload: {
data,
options: { debug: undefined, format: undefined },
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("resetWidget works", () => {
@ -434,22 +421,21 @@ describe("Add functions", () => {
expect(
dataTreeWithFunctions.resetWidget(widgetName, resetChildren),
).resolves.toBe({});
expect(workerEventMock).lastCalledWith({
type: "PROCESS_TRIGGER",
requestId: "EVAL_TRIGGER",
promisified: true,
responseData: {
errors: [],
subRequestId: expect.stringContaining("EVAL_TRIGGER_"),
trigger: {
type: "RESET_WIDGET_META_RECURSIVE_BY_NAME",
payload: {
widgetName,
resetChildren,
expect(workerEventMock).lastCalledWith(
messageCreator("RESET_WIDGET_META_RECURSIVE_BY_NAME", {
data: {
trigger: {
type: "RESET_WIDGET_META_RECURSIVE_BY_NAME",
payload: {
widgetName,
resetChildren,
},
},
eventType: undefined,
},
},
});
method: "PROCESS_TRIGGER",
}),
);
});
it("setInterval works", () => {

View File

@ -1,6 +1,7 @@
import { createGlobalData } from "workers/Evaluation/evaluate";
import _ from "lodash";
jest.mock("../evaluation.worker.ts", () => {
import { MessageType } from "utils/MessageUtil";
jest.mock("../handlers/evalTree", () => {
return {
dataTreeEvaluator: {
evalTree: {},
@ -19,6 +20,12 @@ describe("promise execution", () => {
context: { requestId },
});
const requestMessageCreator = (type: string, body: unknown) => ({
messageId: expect.stringContaining(`${type}_`),
messageType: MessageType.REQUEST,
body,
});
beforeEach(() => {
self.ALLOW_ASYNC = true;
self.postMessage = postMessageMock;
@ -36,21 +43,20 @@ describe("promise execution", () => {
});
it("sends an event from the worker", () => {
dataTreeWithFunctions.showAlert("test alert", "info");
expect(postMessageMock).toBeCalledWith({
requestId,
type: "PROCESS_TRIGGER",
promisified: true,
responseData: expect.objectContaining({
subRequestId: expect.stringContaining(`${requestId}_`),
trigger: {
type: "SHOW_ALERT",
payload: {
message: "test alert",
style: "info",
expect(postMessageMock).toBeCalledWith(
requestMessageCreator("SHOW_ALERT", {
data: {
trigger: {
type: "SHOW_ALERT",
payload: {
message: "test alert",
style: "info",
},
},
},
method: "PROCESS_TRIGGER",
}),
});
);
});
it("returns a promise that resolves", async () => {
postMessageMock.mockReset();
@ -59,15 +65,23 @@ describe("promise execution", () => {
"info",
);
const requestArgs = postMessageMock.mock.calls[0][0];
const subRequestId = requestArgs.responseData.subRequestId;
console.log(requestArgs);
const messageId = requestArgs.messageId;
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: { resolve: ["123"], subRequestId },
method: "PROCESS_TRIGGER",
requestId,
success: true,
messageId,
messageType: MessageType.RESPONSE,
body: {
data: {
data: { resolve: ["123"] },
method: "PROCESS_TRIGGER",
requestId,
success: true,
},
method: "PROCESS_TRIGGER",
},
},
}),
);
@ -82,14 +96,15 @@ describe("promise execution", () => {
"info",
);
const requestArgs = postMessageMock.mock.calls[0][0];
const subRequestId = requestArgs.responseData.subRequestId;
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: { reason: "testing", subRequestId },
method: "PROCESS_TRIGGER",
requestId,
success: false,
messageId: requestArgs.messageId,
messageType: MessageType.RESPONSE,
body: {
data: { data: { reason: "testing" }, success: false },
method: "PROCESS_TRIGGER",
},
},
}),
);
@ -104,20 +119,22 @@ describe("promise execution", () => {
);
const requestArgs = postMessageMock.mock.calls[0][0];
const correctSubRequestId = requestArgs.responseData.subRequestId;
const differentSubRequestId = "wrongRequestId";
const correctId = requestArgs.messageId;
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: {
resolve: ["wrongRequest"],
subRequestId: differentSubRequestId,
messageId: "wrongMessageId",
messageType: MessageType.RESPONSE,
body: {
data: {
data: {
resolve: ["wrongRequest"],
},
success: true,
},
method: "PROCESS_TRIGGER",
},
method: "PROCESS_TRIGGER",
requestId,
success: true,
promisified: true,
},
}),
);
@ -125,10 +142,17 @@ describe("promise execution", () => {
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: { resolve: ["testing"], subRequestId: correctSubRequestId },
method: "PROCESS_TRIGGER",
requestId,
success: true,
messageId: correctId,
messageType: MessageType.RESPONSE,
body: {
data: {
data: {
resolve: ["testing"],
},
success: true,
},
method: "PROCESS_TRIGGER",
},
},
}),
);
@ -143,19 +167,22 @@ describe("promise execution", () => {
);
const requestArgs = postMessageMock.mock.calls[0][0];
const subRequestId = requestArgs.responseData.subRequestId;
const messageId = requestArgs.messageId;
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: {
resolve: ["testing"],
subRequestId,
messageId,
messageType: MessageType.RESPONSE,
body: {
data: {
data: {
resolve: ["testing"],
},
success: true,
},
method: "PROCESS_TRIGGER",
},
method: "PROCESS_TRIGGER",
requestId,
success: true,
promisified: true,
},
}),
);
@ -163,11 +190,12 @@ describe("promise execution", () => {
self.dispatchEvent(
new MessageEvent("message", {
data: {
data: { resolve: ["wrongRequest"], subRequestId },
method: "PROCESS_TRIGGER",
requestId,
success: false,
promisified: true,
messageId,
messageType: MessageType.RESPONSE,
body: {
data: { data: { resolve: ["wrongRequest"] }, success: true },
method: "PROCESS_TRIGGER",
},
},
}),
);

View File

@ -1,5 +1,4 @@
import evaluate, {
setupEvaluationEnvironment,
evaluateAsync,
isFunctionAsync,
} from "workers/Evaluation/evaluate";
@ -9,6 +8,7 @@ import {
ENTITY_TYPE,
} from "entities/DataTree/dataTreeFactory";
import { RenderModes } from "constants/WidgetConstants";
import setupEvalEnv from "../handlers/setupEvalEnv";
describe("evaluateSync", () => {
const widget: DataTreeWidget = {
@ -40,7 +40,7 @@ describe("evaluateSync", () => {
Input1: widget,
};
beforeAll(() => {
setupEvaluationEnvironment();
setupEvalEnv();
});
it("unescapes string before evaluation", () => {
const js = '\\"Hello!\\"';
@ -192,45 +192,32 @@ describe("evaluateAsync", () => {
it("runs and completes", async () => {
const js = "(() => new Promise((resolve) => { resolve(123) }))()";
self.postMessage = jest.fn();
await evaluateAsync(js, {}, "TEST_REQUEST", {});
expect(self.postMessage).toBeCalledWith({
requestId: "TEST_REQUEST",
promisified: true,
responseData: {
finished: true,
result: { errors: [], logs: [], result: 123, triggers: [] },
},
type: "PROCESS_TRIGGER",
const response = await evaluateAsync(js, {}, {}, {});
expect(response).toStrictEqual({
errors: [],
logs: [],
result: 123,
triggers: [],
});
});
it("runs and returns errors", async () => {
jest.restoreAllMocks();
const js = "(() => new Promise((resolve) => { randomKeyword }))()";
self.postMessage = jest.fn();
await evaluateAsync(js, {}, "TEST_REQUEST_1", {});
expect(self.postMessage).toBeCalledWith({
requestId: "TEST_REQUEST_1",
promisified: true,
responseData: {
finished: true,
result: {
errors: [
{
errorMessage: expect.stringContaining(
"randomKeyword is not defined",
),
errorType: "PARSE",
originalBinding: expect.stringContaining("Promise"),
raw: expect.stringContaining("Promise"),
severity: "error",
},
],
triggers: [],
result: undefined,
logs: [],
const result = await evaluateAsync(js, {}, {}, {});
expect(result).toStrictEqual({
errors: [
{
errorMessage: expect.stringContaining("randomKeyword is not defined"),
errorType: "PARSE",
originalBinding: expect.stringContaining("Promise"),
raw: expect.stringContaining("Promise"),
severity: "error",
},
},
type: "PROCESS_TRIGGER",
],
triggers: [],
result: undefined,
logs: [],
});
});
});

View File

@ -1,8 +1,8 @@
import { PluginType } from "entities/Action";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { createGlobalData } from "./evaluate";
import "./TimeoutOverride";
import overrideTimeout from "./TimeoutOverride";
import { createGlobalData } from "../evaluate";
import "../TimeoutOverride";
import overrideTimeout from "../TimeoutOverride";
describe("Expects appsmith setTimeout to pass the following criteria", () => {
overrideTimeout();

View File

@ -0,0 +1,33 @@
export enum EVAL_WORKER_SYNC_ACTION {
SETUP = "SETUP",
EVAL_TREE = "EVAL_TREE",
EVAL_ACTION_BINDINGS = "EVAL_ACTION_BINDINGS",
CLEAR_CACHE = "CLEAR_CACHE",
VALIDATE_PROPERTY = "VALIDATE_PROPERTY",
UNDO = "undo",
REDO = "redo",
UPDATE_REPLAY_OBJECT = "UPDATE_REPLAY_OBJECT",
SET_EVALUATION_VERSION = "SET_EVALUATION_VERSION",
INIT_FORM_EVAL = "INIT_FORM_EVAL",
EXECUTE_SYNC_JS = "EXECUTE_SYNC_JS",
INSTALL_LIBRARY = "INSTALL_LIBRARY",
UNINSTALL_LIBRARY = "UNINSTALL_LIBRARY",
LOAD_LIBRARIES = "LOAD_LIBRARIES",
LINT_TREE = "LINT_TREE",
}
export enum EVAL_WORKER_ASYNC_ACTION {
EVAL_TRIGGER = "EVAL_TRIGGER",
EVAL_EXPRESSION = "EVAL_EXPRESSION",
}
export const EVAL_WORKER_ACTIONS = {
...EVAL_WORKER_SYNC_ACTION,
...EVAL_WORKER_ASYNC_ACTION,
};
export const MAIN_THREAD_ACTION = {
PROCESS_TRIGGER: "PROCESS_TRIGGER",
PROCESS_LOGS: "PROCESS_LOGS",
LINT_TREE: "LINT_TREE",
};

View File

@ -2,22 +2,19 @@
import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
EvaluationError,
extraLibraries,
PropertyEvaluationErrorType,
unsafeFunctionForEval,
} from "utils/DynamicBindingUtils";
import unescapeJS from "unescape-js";
import { LogObject, Severity } from "entities/AppsmithConsole";
import { enhanceDataTreeWithFunctions } from "./Actions";
import { isEmpty } from "lodash";
import { completePromise } from "workers/Evaluation/PromisifyAction";
import { ActionDescription } from "entities/DataTree/actionTriggers";
import userLogs from "./UserLog";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import overrideTimeout from "./TimeoutOverride";
import { TriggerMeta } from "sagas/ActionExecution/ActionExecutionSagas";
import interceptAndOverrideHttpRequest from "./HTTPRequestOverride";
import indirectEval from "./indirectEval";
import { DOM_APIS } from "./SetupDOM";
import { JSLibraries, libraryReservedIdentifiers } from "../common/JSLibrary";
export type EvalResult = {
result: any;
@ -75,11 +72,19 @@ const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => {
function resetWorkerGlobalScope() {
for (const key of Object.keys(self)) {
if (topLevelWorkerAPIs[key]) continue;
if (key === "evaluationVersion") continue;
if (extraLibraries.find((lib) => lib.accessor === key)) continue;
// @ts-expect-error: Types are not available
delete self[key];
if (topLevelWorkerAPIs[key] || DOM_APIS[key]) continue;
//TODO: Remove this once we have a better way to handle this
if (["evaluationVersion", "window", "document", "location"].includes(key))
continue;
if (JSLibraries.find((lib) => lib.accessor.includes(key))) continue;
if (libraryReservedIdentifiers[key]) continue;
try {
// @ts-expect-error: Types are not available
delete self[key];
} catch (e) {
// @ts-expect-error: Types are not available
self[key] = undefined;
}
}
}
@ -98,6 +103,8 @@ export const getScriptType = (
return scriptType;
};
export const additionalLibrariesNames: string[] = [];
export const getScriptToEval = (
userScript: string,
type: EvaluationScriptType,
@ -107,25 +114,7 @@ export const getScriptToEval = (
return `${buffer[0]}${userScript}${buffer[1]}`;
};
export function setupEvaluationEnvironment() {
///// Adding extra libraries separately
extraLibraries.forEach((library) => {
// @ts-expect-error: Types are not available
self[library.accessor] = library.lib;
});
///// Remove all unsafe functions
unsafeFunctionForEval.forEach((func) => {
// @ts-expect-error: Types are not available
self[func] = undefined;
});
userLogs.overrideConsoleAPI();
overrideTimeout();
interceptAndOverrideHttpRequest();
}
const beginsWithLineBreakRegex = /^\s+|\s+$/;
export interface createGlobalDataArgs {
dataTree: DataTree;
resolvedFunctions: Record<string, any>;
@ -165,7 +154,6 @@ export const createGlobalData = (args: createGlobalDataArgs) => {
//// Add internal functions to dataTree;
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(
dataTree,
context?.requestId,
skipEntityFunctions,
context?.eventType,
);
@ -322,7 +310,6 @@ export default function evaluateSync(
export async function evaluateAsync(
userScript: string,
dataTree: DataTree,
requestId: string,
resolvedFunctions: Record<string, any>,
context?: EvaluateContext,
evalArguments?: Array<any>,
@ -335,7 +322,6 @@ export async function evaluateAsync(
/**** Setting the eval context ****/
userLogs.resetLogs();
userLogs.setCurrentRequestInfo({
requestId,
eventType: context?.eventType,
triggerMeta: context?.triggerMeta,
});
@ -343,7 +329,7 @@ export async function evaluateAsync(
dataTree,
resolvedFunctions,
isTriggerBased: true,
context: { ...context, requestId },
context,
evalArguments,
});
const { script } = getUserScriptToEvaluate(userScript, true, evalArguments);
@ -375,21 +361,12 @@ export async function evaluateAsync(
// Adding this extra try catch because there are cases when logs have child objects
// like functions or promises that cause issue in complete promise action, thus
// leading the app into a bad state.
try {
completePromise(requestId, {
result,
errors,
logs,
triggers: Array.from(self.TRIGGER_COLLECTOR),
});
} catch (error) {
completePromise(requestId, {
result,
errors,
logs: [userLogs.parseLogs("log", ["failed to parse logs"])],
triggers: Array.from(self.TRIGGER_COLLECTOR),
});
}
return {
result,
errors,
logs,
triggers: Array.from(self.TRIGGER_COLLECTOR),
};
}
})();
}
@ -419,21 +396,7 @@ export function isFunctionAsync(
const dataTreeKey = GLOBAL_DATA[datum];
if (dataTreeKey) {
const data = dataTreeKey[key]?.data;
//do not remove, we will be investigating this
// const isAsync = dataTreeKey.meta[key]?.isAsync || false;
// const confirmBeforeExecute =
// dataTreeKey.meta[key]?.confirmBeforeExecute || false;
dataTreeKey[key] = resolvedObject[key];
// if (isAsync && confirmBeforeExecute) {
// dataTreeKey[key] = confirmationPromise.bind(
// {},
// "",
// resolvedObject[key],
// key,
// );
// } else {
// dataTreeKey[key] = resolvedObject[key];
// }
if (!!data) {
dataTreeKey[key].data = data;
}

View File

@ -1,427 +1,72 @@
// Workers do not have access to log.error
/* eslint-disable no-console */
import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
DependencyMap,
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
} from "utils/DynamicBindingUtils";
import {
CrashingError,
DataTreeDiff,
getSafeToRenderDataTree,
removeFunctions,
} from "./evaluationUtils";
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import ReplayEntity from "entities/Replay";
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
import ReplayEditor from "entities/Replay/ReplayEntity/ReplayEditor";
import { isEmpty } from "lodash";
import { UserLogObject } from "entities/AppsmithConsole";
import { WorkerErrorTypes } from "workers/common/types";
import {
EvalTreeRequestData,
EvalTreeResponseData,
EvalWorkerRequest,
EvalWorkerResponse,
} from "./types";
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
import { setFormEvaluationSaga } from "workers/Evaluation/formEval";
import evaluate, {
evaluateAsync,
setupEvaluationEnvironment,
} from "./evaluate";
import { JSUpdate } from "utils/JSPaneUtils";
import { validateWidgetProperty } from "workers/common/DataTreeEvaluator/validationUtils";
import { initiateLinting } from "workers/Linting/utils";
import {
createUnEvalTreeForEval,
makeEntityConfigsAsObjProperties,
} from "./dataTreeUtils";
const CANVAS = "canvas";
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
let replayMap: Record<string, ReplayEntity<any>>;
import { EvalWorkerASyncRequest, EvalWorkerSyncRequest } from "./types";
import { syncHandlerMap, asyncHandlerMap } from "./handlers";
import { TMessage, sendMessage, MessageType } from "utils/MessageUtil";
//TODO: Create a more complete RPC setup in the subtree-eval branch.
function messageEventListener(fn: typeof eventRequestHandler) {
return (e: MessageEvent<EvalWorkerRequest>) => {
const startTime = performance.now();
const { method, requestData, requestId } = e.data;
if (method) {
const responseData = fn({ method, requestData, requestId });
if (responseData) {
const endTime = performance.now();
try {
self.postMessage({
requestId,
responseData,
timeTaken: (endTime - startTime).toFixed(2),
});
} catch (e) {
console.error(e);
// we dont want to log dataTree because it is huge.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { dataTree, ...rest } = requestData;
self.postMessage({
requestId,
responseData: {
errors: [
{
type: WorkerErrorTypes.CLONE_ERROR,
message: (e as Error)?.message,
context: JSON.stringify(rest),
},
],
},
timeTaken: (endTime - startTime).toFixed(2),
});
}
}
}
};
function syncRequestMessageListener(
e: MessageEvent<TMessage<EvalWorkerSyncRequest>>,
) {
const { messageType } = e.data;
if (messageType !== MessageType.REQUEST) return;
const startTime = performance.now();
const { body, messageId } = e.data;
const { method } = body;
if (!method) return;
const messageHandler = syncHandlerMap[method];
if (typeof messageHandler !== "function") return;
const responseData = messageHandler(body);
if (!responseData) return;
const endTime = performance.now();
respond(messageId, responseData, endTime - startTime);
}
function eventRequestHandler({
method,
requestData,
requestId,
}: EvalWorkerRequest): EvalWorkerResponse {
switch (method) {
case EVAL_WORKER_ACTIONS.SETUP: {
setupEvaluationEnvironment();
return true;
}
case EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS: {
const { bindings, executionParams } = requestData;
if (!dataTreeEvaluator) {
return { values: undefined, errors: [] };
}
async function asyncRequestMessageListener(
e: MessageEvent<TMessage<EvalWorkerASyncRequest>>,
) {
const { messageType } = e.data;
if (messageType !== MessageType.REQUEST) return;
const start = performance.now();
const { body, messageId } = e.data;
const { method } = body;
if (!method) return;
const messageHandler = asyncHandlerMap[method];
if (typeof messageHandler !== "function") return;
const data = await messageHandler(body);
if (!data) return;
const end = performance.now();
respond(messageId, data, end - start);
}
const values = dataTreeEvaluator.evaluateActionBindings(
bindings,
executionParams,
);
const cleanValues = removeFunctions(values);
const errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
return { values: cleanValues, errors };
}
case EVAL_WORKER_ACTIONS.EVAL_TRIGGER: {
const {
callbackData,
dynamicTrigger,
eventType,
globalContext,
triggerMeta,
unEvalTree: __unEvalTree__,
} = requestData;
if (!dataTreeEvaluator) {
return { triggers: [], errors: [] };
}
const unEvalTree = createUnEvalTreeForEval(__unEvalTree__);
const {
evalOrder,
nonDynamicFieldValidationOrder,
} = dataTreeEvaluator.setupUpdateTree(unEvalTree);
dataTreeEvaluator.evalAndValidateSubTree(
evalOrder,
nonDynamicFieldValidationOrder,
);
const evalTree = dataTreeEvaluator.evalTree;
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
dataTreeEvaluator.evaluateTriggers(
dynamicTrigger,
evalTree,
requestId,
resolvedFunctions,
callbackData,
{
globalContext,
eventType,
triggerMeta,
function respond(messageId: string, data: unknown, timeTaken: number) {
try {
sendMessage.call(self, {
messageId,
messageType: MessageType.RESPONSE,
body: { data, timeTaken },
});
} catch (e) {
console.error(e);
sendMessage.call(self, {
messageId,
messageType: MessageType.RESPONSE,
body: {
timeTaken: timeTaken.toFixed(2),
data: {
errors: [
{
type: WorkerErrorTypes.CLONE_ERROR,
message: (e as Error)?.message,
context: JSON.stringify(data),
},
],
},
);
break;
}
case EVAL_WORKER_ACTIONS.PROCESS_TRIGGER:
case EVAL_WORKER_ACTIONS.LINT_TREE:
/**
* These actions will not be processed here. They will be handled in the eval trigger sub steps
* @link promisifyAction
**/
break;
case EVAL_WORKER_ACTIONS.CLEAR_CACHE: {
dataTreeEvaluator = undefined;
return true;
}
case EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY: {
const { property, props, validation, value } = requestData;
return removeFunctions(
validateWidgetProperty(validation, value, props, property),
);
}
case EVAL_WORKER_ACTIONS.UNDO: {
const { entityId } = requestData;
if (!replayMap[entityId || CANVAS]) return;
const replayResult = replayMap[entityId || CANVAS].replay("UNDO");
replayMap[entityId || CANVAS].clearLogs();
return replayResult;
}
case EVAL_WORKER_ACTIONS.REDO: {
const { entityId } = requestData;
if (!replayMap[entityId ?? CANVAS]) return;
const replayResult = replayMap[entityId ?? CANVAS].replay("REDO");
replayMap[entityId ?? CANVAS].clearLogs();
return replayResult;
}
case EVAL_WORKER_ACTIONS.EXECUTE_SYNC_JS: {
const { functionCall } = requestData;
if (!dataTreeEvaluator) {
return true;
}
const evalTree = dataTreeEvaluator.evalTree;
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
const { errors, logs, result } = evaluate(
functionCall,
evalTree,
resolvedFunctions,
false,
undefined,
);
return { errors, logs, result };
}
case EVAL_WORKER_ACTIONS.EVAL_EXPRESSION:
const { expression, isTrigger } = requestData;
const evalTree = dataTreeEvaluator?.evalTree;
if (!evalTree) return {};
// TODO find a way to do this for snippets
return isTrigger
? evaluateAsync(expression, evalTree, "SNIPPET", {})
: evaluate(expression, evalTree, {}, false);
case EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT:
const { entity, entityId, entityType } = requestData;
const replayObject = replayMap[entityId];
if (replayObject) {
replayObject.update(entity);
} else {
replayMap[entityId] = new ReplayEditor(entity, entityType);
}
break;
case EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION:
const { version } = requestData;
self.evaluationVersion = version || 1;
break;
case EVAL_WORKER_ACTIONS.INIT_FORM_EVAL:
const { currentEvalState, payload, type } = requestData;
const response = setFormEvaluationSaga(type, payload, currentEvalState);
return response;
case EVAL_WORKER_ACTIONS.EVAL_TREE: {
let evalOrder: string[] = [];
let lintOrder: string[] = [];
let jsUpdates: Record<string, JSUpdate> = {};
let unEvalUpdates: DataTreeDiff[] = [];
let nonDynamicFieldValidationOrder: string[] = [];
let isCreateFirstTree = false;
let dataTree: DataTree = {};
let errors: EvalError[] = [];
let logs: any[] = [];
let userLogs: UserLogObject[] = [];
let dependencies: DependencyMap = {};
let evalMetaUpdates: EvalMetaUpdates = [];
const {
allActionValidationConfig,
requiresLinting,
shouldReplay,
theme,
unevalTree: __unevalTree__,
widgets,
widgetTypeConfigMap,
} = requestData as EvalTreeRequestData;
const unevalTree = createUnEvalTreeForEval(__unevalTree__);
try {
if (!dataTreeEvaluator) {
isCreateFirstTree = true;
replayMap = replayMap || {};
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
dataTreeEvaluator = new DataTreeEvaluator(
widgetTypeConfigMap,
allActionValidationConfig,
);
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
unevalTree,
);
evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder;
jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(
dataTreeResponse.evalTree,
{
evalProps: dataTreeEvaluator.evalProps,
},
);
} else if (dataTreeEvaluator.hasCyclicalDependency) {
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
//allActionValidationConfigs may not be set in dataTreeEvaluatior. Therefore, set it explicitly via setter method
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
if (shouldReplay) {
replayMap[CANVAS]?.update({ widgets, theme });
}
dataTreeEvaluator = new DataTreeEvaluator(
widgetTypeConfigMap,
allActionValidationConfig,
);
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
unevalTree,
);
isCreateFirstTree = true;
evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder;
jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(
dataTreeResponse.evalTree,
{
evalProps: dataTreeEvaluator.evalProps,
},
);
} else {
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
isCreateFirstTree = false;
if (shouldReplay) {
replayMap[CANVAS]?.update({ widgets, theme });
}
const setupUpdateTreeResponse = dataTreeEvaluator.setupUpdateTree(
unevalTree,
);
evalOrder = setupUpdateTreeResponse.evalOrder;
lintOrder = setupUpdateTreeResponse.lintOrder;
jsUpdates = setupUpdateTreeResponse.jsUpdates;
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
nonDynamicFieldValidationOrder =
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
const updateResponse = dataTreeEvaluator.evalAndValidateSubTree(
evalOrder,
nonDynamicFieldValidationOrder,
);
dataTree = makeEntityConfigsAsObjProperties(
dataTreeEvaluator.evalTree,
{
evalProps: dataTreeEvaluator.evalProps,
},
);
evalMetaUpdates = JSON.parse(
JSON.stringify(updateResponse.evalMetaUpdates),
);
}
dataTreeEvaluator = dataTreeEvaluator as DataTreeEvaluator;
dependencies = dataTreeEvaluator.inverseDependencyMap;
errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
logs = dataTreeEvaluator.logs;
userLogs = dataTreeEvaluator.userLogs;
if (shouldReplay) {
if (replayMap[CANVAS]?.logs)
logs = logs.concat(replayMap[CANVAS]?.logs);
replayMap[CANVAS]?.clearLogs();
}
dataTreeEvaluator.clearLogs();
} catch (error) {
if (dataTreeEvaluator !== undefined) {
errors = dataTreeEvaluator.errors;
logs = dataTreeEvaluator.logs;
userLogs = dataTreeEvaluator.userLogs;
}
if (!(error instanceof CrashingError)) {
errors.push({
type: EvalErrorTypes.UNKNOWN_ERROR,
message: (error as Error).message,
});
console.error(error);
}
dataTree = getSafeToRenderDataTree(
makeEntityConfigsAsObjProperties(unevalTree, {
sanitizeDataTree: false,
evalProps: dataTreeEvaluator?.evalProps,
}),
widgetTypeConfigMap,
);
unEvalUpdates = [];
}
return {
dataTree,
dependencies,
errors,
evalMetaUpdates,
evaluationOrder: evalOrder,
jsUpdates,
logs,
userLogs,
unEvalUpdates,
isCreateFirstTree,
} as EvalTreeResponseData;
}
default: {
console.error("Action not registered on evalWorker", method);
}
},
});
}
}
self.onmessage = messageEventListener(eventRequestHandler);
self.addEventListener("message", syncRequestMessageListener);
self.addEventListener("message", asyncRequestMessageListener);

View File

@ -0,0 +1,94 @@
import { installLibrary, uninstallLibrary } from "../jsLibrary";
import { EVAL_WORKER_SYNC_ACTION } from "workers/Evaluation/evalWorkerActions";
import * as mod from "../../../common/JSLibrary/ternDefinitionGenerator";
jest.mock("../../../common/JSLibrary/ternDefinitionGenerator");
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 = {};
});
const mockTernDefsGenerator = jest.fn(() => ({}));
jest.mock("../../../common/JSLibrary/ternDefinitionGenerator.ts", () => {
return {
makeTernDefs: mockTernDefsGenerator,
};
});
});
it("should install a library", function() {
const res = installLibrary({
data: {
url:
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js",
takenAccessors: [],
takenNamesMap: {},
},
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
});
//
expect(self.importScripts).toHaveBeenCalled();
expect(mod.makeTernDefs).toHaveBeenCalledWith({});
expect(res).toEqual({
success: true,
defs: {
"!name": "LIB/lodash",
lodash: undefined,
},
accessor: ["lodash"],
});
});
it("Reinstalling a different version of the same installed library should fail", function() {
const res = installLibrary({
data: {
url:
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
takenAccessors: ["lodash"],
takenNamesMap: {},
},
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
});
expect(res).toEqual({
success: false,
defs: {},
error: expect.any(Error),
});
});
it("Detects name space collision where there is another entity(api, widget or query) with the same name", function() {
//@ts-expect-error ignore
delete self.lodash;
const res = installLibrary({
data: {
url:
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.0/lodash.min.js",
takenAccessors: [],
takenNamesMap: { lodash: true },
},
method: EVAL_WORKER_SYNC_ACTION.INSTALL_LIBRARY,
});
expect(res).toEqual({
success: false,
defs: {},
error: expect.any(Error),
});
});
it("Removes or set the accessors to undefined on the global object on uninstallation", function() {
//@ts-expect-error ignore
self.lodash = {};
const res = 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

@ -0,0 +1,22 @@
import { dataTreeEvaluator } from "./evalTree";
import { removeFunctions } from "../evaluationUtils";
import { EvalWorkerSyncRequest } from "../types";
export default function(request: EvalWorkerSyncRequest) {
const { data } = request;
const { bindings, executionParams } = data;
if (!dataTreeEvaluator) {
return { values: undefined, errors: [] };
}
const values = dataTreeEvaluator.evaluateActionBindings(
bindings,
executionParams,
);
const cleanValues = removeFunctions(values);
const errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
return { values: cleanValues, errors };
}

View File

@ -0,0 +1,12 @@
import { evaluateAsync } from "../evaluate";
import { EvalWorkerASyncRequest } from "../types";
import { dataTreeEvaluator } from "./evalTree";
export default function(request: EvalWorkerASyncRequest) {
const { data } = request;
const { expression } = data;
const evalTree = dataTreeEvaluator?.evalTree;
const resolvedFunctions = dataTreeEvaluator?.resolvedFunctions || {};
if (!evalTree) return {};
return evaluateAsync(expression, evalTree, resolvedFunctions, {});
}

View File

@ -0,0 +1,220 @@
import { UserLogObject } from "entities/AppsmithConsole";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import ReplayEntity from "entities/Replay";
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
import { isEmpty } from "lodash";
import {
DependencyMap,
EvalError,
EvalErrorTypes,
} from "utils/DynamicBindingUtils";
import { JSUpdate } from "utils/JSPaneUtils";
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
import { initiateLinting } from "workers/Linting/utils";
import {
createUnEvalTreeForEval,
makeEntityConfigsAsObjProperties,
} from "../dataTreeUtils";
import {
CrashingError,
DataTreeDiff,
getSafeToRenderDataTree,
} from "../evaluationUtils";
import {
EvalTreeRequestData,
EvalTreeResponseData,
EvalWorkerSyncRequest,
} from "../types";
export let replayMap: Record<string, ReplayEntity<any>>;
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
export const CANVAS = "canvas";
export default function(request: EvalWorkerSyncRequest) {
const { data } = request;
let evalOrder: string[] = [];
let lintOrder: string[] = [];
let jsUpdates: Record<string, JSUpdate> = {};
let unEvalUpdates: DataTreeDiff[] = [];
let nonDynamicFieldValidationOrder: string[] = [];
let isCreateFirstTree = false;
let dataTree: DataTree = {};
let errors: EvalError[] = [];
let logs: any[] = [];
let userLogs: UserLogObject[] = [];
let dependencies: DependencyMap = {};
let evalMetaUpdates: EvalMetaUpdates = [];
const {
allActionValidationConfig,
forceEvaluation,
requiresLinting,
shouldReplay,
theme,
unevalTree: __unevalTree__,
widgets,
widgetTypeConfigMap,
} = data as EvalTreeRequestData;
const unevalTree = createUnEvalTreeForEval(__unevalTree__);
try {
if (!dataTreeEvaluator) {
isCreateFirstTree = true;
replayMap = replayMap || {};
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
dataTreeEvaluator = new DataTreeEvaluator(
widgetTypeConfigMap,
allActionValidationConfig,
);
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
unevalTree,
);
evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder;
jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
});
} else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) {
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
//allActionValidationConfigs may not be set in dataTreeEvaluatior. Therefore, set it explicitly via setter method
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
if (shouldReplay) {
replayMap[CANVAS]?.update({ widgets, theme });
}
dataTreeEvaluator = new DataTreeEvaluator(
widgetTypeConfigMap,
allActionValidationConfig,
);
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
const setupFirstTreeResponse = dataTreeEvaluator.setupFirstTree(
unevalTree,
);
isCreateFirstTree = true;
evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder;
jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
});
} else {
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
dataTreeEvaluator.setAllActionValidationConfig(
allActionValidationConfig,
);
}
isCreateFirstTree = false;
if (shouldReplay) {
replayMap[CANVAS]?.update({ widgets, theme });
}
const setupUpdateTreeResponse = dataTreeEvaluator.setupUpdateTree(
unevalTree,
);
evalOrder = setupUpdateTreeResponse.evalOrder;
lintOrder = setupUpdateTreeResponse.lintOrder;
jsUpdates = setupUpdateTreeResponse.jsUpdates;
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
initiateLinting(
lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
sanitizeDataTree: false,
}),
requiresLinting,
);
nonDynamicFieldValidationOrder =
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
const updateResponse = dataTreeEvaluator.evalAndValidateSubTree(
evalOrder,
nonDynamicFieldValidationOrder,
);
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
});
evalMetaUpdates = JSON.parse(
JSON.stringify(updateResponse.evalMetaUpdates),
);
}
dataTreeEvaluator = dataTreeEvaluator as DataTreeEvaluator;
dependencies = dataTreeEvaluator.inverseDependencyMap;
errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
logs = dataTreeEvaluator.logs;
userLogs = dataTreeEvaluator.userLogs;
if (shouldReplay) {
if (replayMap[CANVAS]?.logs) logs = logs.concat(replayMap[CANVAS]?.logs);
replayMap[CANVAS]?.clearLogs();
}
dataTreeEvaluator.clearLogs();
} catch (error) {
if (dataTreeEvaluator !== undefined) {
errors = dataTreeEvaluator.errors;
logs = dataTreeEvaluator.logs;
userLogs = dataTreeEvaluator.userLogs;
}
if (!(error instanceof CrashingError)) {
errors.push({
type: EvalErrorTypes.UNKNOWN_ERROR,
message: (error as Error).message,
});
// eslint-disable-next-line
console.error(error);
}
dataTree = getSafeToRenderDataTree(
makeEntityConfigsAsObjProperties(unevalTree, {
sanitizeDataTree: false,
evalProps: dataTreeEvaluator?.evalProps,
}),
widgetTypeConfigMap,
);
unEvalUpdates = [];
}
return {
dataTree,
dependencies,
errors,
evalMetaUpdates,
evaluationOrder: evalOrder,
jsUpdates,
logs,
userLogs,
unEvalUpdates,
isCreateFirstTree,
} as EvalTreeResponseData;
}
export function clearCache() {
dataTreeEvaluator = undefined;
return true;
}

View File

@ -0,0 +1,41 @@
import { dataTreeEvaluator } from "./evalTree";
import { EvalWorkerASyncRequest } from "../types";
import { createUnEvalTreeForEval } from "../dataTreeUtils";
export default async function(request: EvalWorkerASyncRequest) {
const { data } = request;
const {
callbackData,
dynamicTrigger,
eventType,
globalContext,
triggerMeta,
unEvalTree: __unEvalTree__,
} = data;
if (!dataTreeEvaluator) {
return { triggers: [], errors: [] };
}
const unEvalTree = createUnEvalTreeForEval(__unEvalTree__);
const {
evalOrder,
nonDynamicFieldValidationOrder,
} = dataTreeEvaluator.setupUpdateTree(unEvalTree);
dataTreeEvaluator.evalAndValidateSubTree(
evalOrder,
nonDynamicFieldValidationOrder,
);
const evalTree = dataTreeEvaluator.evalTree;
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
return dataTreeEvaluator.evaluateTriggers(
dynamicTrigger,
evalTree,
resolvedFunctions,
callbackData,
{
globalContext,
eventType,
triggerMeta,
},
);
}

View File

@ -0,0 +1,21 @@
import evaluateSync from "../evaluate";
import { dataTreeEvaluator } from "./evalTree";
import { EvalWorkerSyncRequest } from "../types";
export default function(request: EvalWorkerSyncRequest) {
const { data } = request;
const { functionCall } = data;
if (!dataTreeEvaluator) {
return true;
}
const evalTree = dataTreeEvaluator.evalTree;
const resolvedFunctions = dataTreeEvaluator.resolvedFunctions;
return evaluateSync(
functionCall,
evalTree,
resolvedFunctions,
false,
undefined,
);
}

View File

@ -0,0 +1,50 @@
import noop from "lodash/noop";
import {
EVAL_WORKER_ACTIONS,
EVAL_WORKER_ASYNC_ACTION,
EVAL_WORKER_SYNC_ACTION,
} from "workers/Evaluation/evalWorkerActions";
import { EvalWorkerSyncRequest, EvalWorkerASyncRequest } from "../types";
import evalActionBindings from "./evalActionBindings";
import evalExpression from "./evalExpression";
import evalTree, { clearCache } from "./evalTree";
import evalTrigger from "./evalTrigger";
import executeSyncJS from "./executeSyncJS";
import initFormEval from "./initFormEval";
import { installLibrary, loadLibraries, uninstallLibrary } from "./jsLibrary";
import { redo, undo, updateReplayObject } from "./replay";
import setupEvaluationEnvironment, {
setEvaluationVersion,
} from "./setupEvalEnv";
import validateProperty from "./validateProperty";
const syncHandlerMap: Record<
EVAL_WORKER_SYNC_ACTION,
(req: EvalWorkerSyncRequest) => any
> = {
[EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS]: evalActionBindings,
[EVAL_WORKER_ACTIONS.EVAL_TREE]: evalTree,
[EVAL_WORKER_ACTIONS.EXECUTE_SYNC_JS]: executeSyncJS,
[EVAL_WORKER_ACTIONS.UNDO]: undo,
[EVAL_WORKER_ACTIONS.REDO]: redo,
[EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT]: updateReplayObject,
[EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY]: validateProperty,
[EVAL_WORKER_ACTIONS.INSTALL_LIBRARY]: installLibrary,
[EVAL_WORKER_ACTIONS.UNINSTALL_LIBRARY]: uninstallLibrary,
[EVAL_WORKER_ACTIONS.LOAD_LIBRARIES]: loadLibraries,
[EVAL_WORKER_ACTIONS.LINT_TREE]: noop,
[EVAL_WORKER_ACTIONS.SETUP]: setupEvaluationEnvironment,
[EVAL_WORKER_ACTIONS.CLEAR_CACHE]: clearCache,
[EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION]: setEvaluationVersion,
[EVAL_WORKER_ACTIONS.INIT_FORM_EVAL]: initFormEval,
};
const asyncHandlerMap: Record<
EVAL_WORKER_ASYNC_ACTION,
(req: EvalWorkerASyncRequest) => any
> = {
[EVAL_WORKER_ACTIONS.EVAL_TRIGGER]: evalTrigger,
[EVAL_WORKER_ACTIONS.EVAL_EXPRESSION]: evalExpression,
};
export { syncHandlerMap, asyncHandlerMap };

View File

@ -0,0 +1,9 @@
import { setFormEvaluationSaga } from "../formEval";
import { EvalWorkerSyncRequest } from "../types";
export default function(request: EvalWorkerSyncRequest) {
const { data } = request;
const { currentEvalState, payload, type } = data;
const response = setFormEvaluationSaga(type, payload, currentEvalState);
return response;
}

View File

@ -0,0 +1,202 @@
import {
createMessage,
customJSLibraryMessages,
} from "@appsmith/constants/messages";
import difference from "lodash/difference";
import { Def } from "tern";
import {
JSLibraries,
libraryReservedIdentifiers,
} from "../../common/JSLibrary";
import { makeTernDefs } from "../../common/JSLibrary/ternDefinitionGenerator";
import { EvalWorkerSyncRequest } from "../types";
enum LibraryInstallError {
NameCollisionError,
ImportError,
TernDefinitionError,
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) {
super(createMessage(customJSLibraryMessages.IMPORT_URL_ERROR, url));
this.name = "ImportError";
}
}
class TernDefinitionError extends Error {
code = LibraryInstallError.TernDefinitionError;
constructor(name: string) {
super(createMessage(customJSLibraryMessages.DEFS_FAILED_ERROR, name));
this.name = "TernDefinitionError";
}
}
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;
}
}
export function installLibrary(request: EvalWorkerSyncRequest) {
const { data } = request;
const { takenAccessors, takenNamesMap, url } = data;
const defs: Def = {};
try {
const currentEnvKeys = Object.keys(self);
//@ts-expect-error Find libraries that were uninstalled.
const unsetKeys = currentEnvKeys.filter((key) => self[key] === undefined);
const existingLibraries: Record<string, any> = {};
for (const acc of takenAccessors) {
existingLibraries[acc] = self[acc];
}
try {
self.importScripts(url);
} catch (e) {
throw new ImportError(url);
}
// Find keys add that were installed to the global scope.
const accessor = difference(Object.keys(self), currentEnvKeys) as Array<
string
>;
checkForNameCollision(accessor, takenNamesMap);
checkIfUninstalledEarlier(accessor, unsetKeys);
checkForOverrides(url, accessor, takenAccessors, existingLibraries);
if (accessor.length === 0) return { status: false, defs, accessor };
//Reserves accessor names.
const name = accessor[accessor.length - 1];
defs["!name"] = `LIB/${name}`;
try {
for (const key of accessor) {
//@ts-expect-error no types
defs[key] = makeTernDefs(self[key]);
}
} catch (e) {
for (const acc of accessor) {
//@ts-expect-error no types
self[acc] = undefined;
}
throw new TernDefinitionError(
`Failed to generate autocomplete definitions: ${name}`,
);
}
//Reserve accessor names.
for (const acc of accessor) {
libraryReservedIdentifiers[acc] = true;
}
return { success: true, defs, accessor };
} catch (error) {
return { success: false, defs, error };
}
}
export function uninstallLibrary(request: EvalWorkerSyncRequest) {
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;
}
delete libraryReservedIdentifiers[key];
}
return { success: true };
} catch (e) {
return { success: false };
}
}
export function loadLibraries(request: EvalWorkerSyncRequest) {
//Add types
const { data } = request;
const urls = data.map((lib: any) => lib.url);
const keysBefore = Object.keys(self);
let message = "";
try {
self.importScripts(...urls);
} catch (e) {
message = (e as Error).message;
}
const keysAfter = Object.keys(self);
const newKeys = difference(keysAfter, keysBefore);
for (const key of newKeys) {
libraryReservedIdentifiers[key] = true;
}
JSLibraries.push(...data);
return { success: !message, message };
}
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);
}

View File

@ -0,0 +1,33 @@
import ReplayEditor from "entities/Replay/ReplayEntity/ReplayEditor";
import { EvalWorkerSyncRequest } from "../types";
import { CANVAS, replayMap } from "./evalTree";
export function undo(request: EvalWorkerSyncRequest) {
const { data } = request;
const { entityId } = data;
if (!replayMap[entityId || CANVAS]) return;
const replayResult = replayMap[entityId || CANVAS].replay("UNDO");
replayMap[entityId || CANVAS].clearLogs();
return replayResult;
}
export function redo(request: EvalWorkerSyncRequest) {
const { data } = request;
const { entityId } = data;
if (!replayMap[entityId ?? CANVAS]) return;
const replayResult = replayMap[entityId ?? CANVAS].replay("REDO");
replayMap[entityId ?? CANVAS].clearLogs();
return replayResult;
}
export function updateReplayObject(request: EvalWorkerSyncRequest) {
const { data } = request;
const { entity, entityId, entityType } = data;
const replayObject = replayMap[entityId];
if (replayObject) {
replayObject.update(entity);
} else {
replayMap[entityId] = new ReplayEditor(entity, entityType);
}
return true;
}

View File

@ -0,0 +1,37 @@
import { unsafeFunctionForEval } from "utils/DynamicBindingUtils";
import interceptAndOverrideHttpRequest from "../HTTPRequestOverride";
import { resetJSLibraries } from "../../common/JSLibrary";
import setupDOM from "../SetupDOM";
import overrideTimeout from "../TimeoutOverride";
import { EvalWorkerSyncRequest } from "../types";
import userLogs from "../UserLog";
export default function() {
const libraries = resetJSLibraries();
///// Adding extra libraries separately
libraries.forEach((library) => {
// @ts-expect-error: Types are not available
self[library.accessor] = library.lib;
});
///// Remove all unsafe functions
unsafeFunctionForEval.forEach((func) => {
// @ts-expect-error: Types are not available
self[func] = undefined;
});
self.window = self;
userLogs.overrideConsoleAPI();
overrideTimeout();
interceptAndOverrideHttpRequest();
setupDOM();
return true;
}
export function setEvaluationVersion(request: EvalWorkerSyncRequest) {
const { data } = request;
const { version } = data;
self.evaluationVersion = version || 1;
// TODO: Move this to setup
resetJSLibraries();
return true;
}

View File

@ -0,0 +1,11 @@
import { validateWidgetProperty } from "workers/common/DataTreeEvaluator/validationUtils";
import { removeFunctions } from "../evaluationUtils";
import { EvalWorkerSyncRequest } from "../types";
export default function(request: EvalWorkerSyncRequest) {
const { data } = request;
const { property, props, validation, value } = data;
return removeFunctions(
validateWidgetProperty(validation, value, props, property),
);
}

View File

@ -4,18 +4,22 @@ import { AppTheme } from "entities/AppTheming";
import { DataTree, UnEvalTree } from "entities/DataTree/dataTreeFactory";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
import {
DependencyMap,
EvalError,
EVAL_WORKER_ACTIONS,
} from "utils/DynamicBindingUtils";
EVAL_WORKER_ASYNC_ACTION,
EVAL_WORKER_SYNC_ACTION,
} from "workers/Evaluation/evalWorkerActions";
import { JSUpdate } from "utils/JSPaneUtils";
import { WidgetTypeConfigMap } from "utils/WidgetFactory";
import { EvalMetaUpdates } from "workers/common/DataTreeEvaluator/types";
import { WorkerRequest } from "workers/common/types";
import { DataTreeDiff } from "./evaluationUtils";
export type EvalWorkerRequest = WorkerRequest<any, EVAL_WORKER_ACTIONS>;
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
export type EvalWorkerASyncRequest = WorkerRequest<
any,
EVAL_WORKER_ASYNC_ACTION
>;
export type EvalWorkerResponse = EvalTreeResponseData | boolean | unknown;
export interface EvalTreeRequestData {
@ -28,6 +32,7 @@ export interface EvalTreeRequestData {
[actionId: string]: ActionValidationConfigMap;
};
requiresLinting: boolean;
forceEvaluation: boolean;
}
export interface EvalTreeResponseData {
dataTree: DataTree;

View File

@ -1,4 +1,6 @@
import { isEqual } from "lodash";
import { WorkerErrorTypes } from "workers/common/types";
import { JSLibraries, resetJSLibraries } from "workers/common/JSLibrary";
import {
LintWorkerRequest,
LintTreeResponse,
@ -6,37 +8,47 @@ import {
LintTreeRequest,
} from "./types";
import { getlintErrorsFromTree } from "./utils";
import { TMessage, MessageType, sendMessage } from "utils/MessageUtil";
function messageEventListener(fn: typeof eventRequestHandler) {
return (event: MessageEvent<LintWorkerRequest>) => {
const { method, requestId } = event.data;
return (event: MessageEvent<TMessage<LintWorkerRequest>>) => {
const { messageType } = event.data;
if (messageType !== MessageType.REQUEST) return;
const { body, messageId } = event.data;
const { data, method } = body;
if (!method) return;
const startTime = performance.now();
const responseData = fn(event.data);
const responseData = fn({ method, requestData: data });
const endTime = performance.now();
if (!responseData) return;
try {
self.postMessage({
requestId,
responseData,
timeTaken: (endTime - startTime).toFixed(2),
sendMessage.call(self, {
messageId,
messageType: MessageType.RESPONSE,
body: {
data: responseData,
timeTaken: (endTime - startTime).toFixed(2),
},
});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
self.postMessage({
requestId,
responseData: {
errors: [
{
type: WorkerErrorTypes.CLONE_ERROR,
message: (e as Error)?.message,
},
],
sendMessage.call(self, {
messageId,
messageType: MessageType.RESPONSE,
body: {
data: {
errors: [
{
type: WorkerErrorTypes.CLONE_ERROR,
message: (e as Error)?.message,
},
],
},
timeTaken: (endTime - startTime).toFixed(2),
},
timeTaken: (endTime - startTime).toFixed(2),
});
}
};
@ -45,7 +57,10 @@ function messageEventListener(fn: typeof eventRequestHandler) {
function eventRequestHandler({
method,
requestData,
}: LintWorkerRequest): LintTreeResponse | unknown {
}: {
method: LINT_WORKER_ACTIONS;
requestData: any;
}): LintTreeResponse | unknown {
switch (method) {
case LINT_WORKER_ACTIONS.LINT_TREE: {
const lintTreeResponse: LintTreeResponse = { errors: {} };
@ -56,7 +71,24 @@ function eventRequestHandler({
} catch (e) {}
return lintTreeResponse;
}
case LINT_WORKER_ACTIONS.UPDATE_LINT_GLOBALS: {
const { add, libs } = requestData;
if (add) {
JSLibraries.push(...libs);
} else if (add === false) {
for (const lib of libs) {
const idx = JSLibraries.findIndex((l) =>
isEqual(l.accessor.sort(), lib.accessor.sort()),
);
if (idx === -1) return;
JSLibraries.splice(idx, 1);
}
} else {
resetJSLibraries();
JSLibraries.push(...libs);
}
return true;
}
default: {
// eslint-disable-next-line no-console
console.error("Action not registered on lintWorker ", method);

View File

@ -4,6 +4,7 @@ import { WorkerRequest } from "workers/common/types";
export enum LINT_WORKER_ACTIONS {
LINT_TREE = "LINT_TREE",
UPDATE_LINT_GLOBALS = "UPDATE_LINT_GLOBALS",
}
export interface LintTreeResponse {

View File

@ -2,13 +2,12 @@ import { DataTree, DataTreeEntity } from "entities/DataTree/dataTreeFactory";
import { Position } from "codemirror";
import {
EVAL_WORKER_ACTIONS,
extraLibraries,
isDynamicValue,
isPathADynamicBinding,
LintError,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { MAIN_THREAD_ACTION } from "workers/Evaluation/evalWorkerActions";
import {
JSHINT as jshint,
LintError as JSHintError,
@ -52,6 +51,8 @@ import {
} from "workers/Evaluation/evaluationUtils";
import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import { Severity } from "entities/AppsmithConsole";
import { JSLibraries } from "workers/common/JSLibrary";
import { MessageType, sendMessage } from "utils/MessageUtil";
export function getlintErrorsFromTree(
pathsToLint: string[],
@ -302,7 +303,11 @@ export function getLintingErrors(
globalData[dataKey] = true;
}
// Jshint shouldn't throw errors for additional libraries
extraLibraries.forEach((lib) => (globalData[lib.accessor] = true));
const libAccessors = ([] as string[]).concat(
...JSLibraries.map((lib) => lib.accessor),
);
libAccessors.forEach((accessor) => (globalData[accessor] = true));
// JSHint shouldn't throw errors for supported web apis
Object.keys(SUPPORTED_WEB_APIS).forEach(
(apiName) => (globalData[apiName] = true),
@ -464,12 +469,15 @@ export function initiateLinting(
requiresLinting: boolean,
) {
if (!requiresLinting) return;
postMessage({
promisified: true,
responseData: {
lintOrder,
unevalTree,
type: EVAL_WORKER_ACTIONS.LINT_TREE,
sendMessage.call(self, {
messageId: "",
messageType: MessageType.REQUEST,
body: {
data: {
lintOrder,
unevalTree,
},
method: MAIN_THREAD_ACTION.LINT_TREE,
},
});
}

View File

@ -38,7 +38,6 @@ function getFile(file: string, c: CallbackFn) {
}
function startServer(defs: Def[], plugins = {}, scripts?: string[]) {
//@ts-expect-error test
if (scripts) self.importScripts.apply(null, scripts);
server = new tern.Server({

View File

@ -1013,7 +1013,6 @@ export default class DataTreeEvaluator {
async evaluateTriggers(
userScript: string,
dataTree: DataTree,
requestId: string,
resolvedFunctions: Record<string, any>,
callbackData: Array<unknown>,
context?: EvaluateContext,
@ -1022,7 +1021,6 @@ export default class DataTreeEvaluator {
return evaluateAsync(
jsSnippets[0] || userScript,
dataTree,
requestId,
resolvedFunctions,
context,
callbackData,

View File

@ -5,7 +5,6 @@ import {
EvalError,
DependencyMap,
getDynamicBindings,
extraLibrariesNames,
getEntityDynamicBindingPathList,
} from "utils/DynamicBindingUtils";
import { extractIdentifierInfoFromCode } from "@shared/ast";
@ -26,6 +25,7 @@ import {
JAVASCRIPT_KEYWORDS,
} from "constants/WidgetValidation";
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
import { libraryReservedIdentifiers } from "workers/common/JSLibrary";
/** This function extracts validReferences and invalidReferences from a binding {{}}
* @param script
@ -45,7 +45,7 @@ export const extractInfoFromBinding = (
const { references } = extractIdentifierInfoFromCode(
script,
self.evaluationVersion,
invalidEntityIdentifiers,
{ ...invalidEntityIdentifiers, ...libraryReservedIdentifiers },
);
return extractInfoFromReferences(references, allPaths);
};
@ -207,7 +207,6 @@ const invalidEntityIdentifiers: Record<string, unknown> = {
...JAVASCRIPT_KEYWORDS,
...APPSMITH_GLOBAL_FUNCTIONS,
...DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS,
...extraLibrariesNames,
};
export function listEntityDependencies(

View File

@ -0,0 +1,62 @@
import { makeTernDefs } from "../ternDefinitionGenerator";
describe("Tests tern definition generator", () => {
const obj = {
var1: "myVar1",
var2: 2,
var3: true,
var4: null,
var5: undefined,
var6: { a: 1, b: 2 },
var7: () => {
return "there!";
},
var8: function() {
return "hey, ";
},
var9: new Date(),
};
const proto = {
sayHello() {
return "Hello";
},
};
it("Correctly determines tern def types based", () => {
const expected = {
var1: { "!type": "string" },
var2: { "!type": "number" },
var3: { "!type": "bool" },
var4: { "!type": "?" },
var5: { "!type": "?" },
var6: { a: { "!type": "number" }, b: { "!type": "number" } },
var7: { "!type": "fn()" },
};
const defs = makeTernDefs(obj);
expect(defs).toMatchObject(expected);
});
it("should look up the prototype chain on objects", () => {
Object.setPrototypeOf(obj, proto);
const expected = {
sayHello: { "!type": "fn()" },
};
const defs = makeTernDefs(proto);
expect(defs).toMatchObject(expected);
});
it("should look up the prototype property on functions", () => {
obj.var8.prototype = {
sayWorld() {
return "World";
},
};
const expected = {
var8: {
"!type": "fn()",
prototype: {
sayWorld: { "!type": "fn()" },
},
},
};
const defs = makeTernDefs(obj);
expect(defs).toMatchObject(expected);
});
});

View File

@ -0,0 +1,74 @@
import _, { VERSION as lodashVersion } from "lodash";
import moment from "moment-timezone";
import parser from "fast-xml-parser";
import forge from "node-forge";
export type TJSLibrary = {
version?: string;
docsURL: string;
name: string;
accessor: string[];
lib?: any;
url?: string;
};
export const defaultLibraries: TJSLibrary[] = [
{
accessor: ["_"],
lib: _,
version: lodashVersion,
docsURL: `https://lodash.com/docs/${lodashVersion}`,
name: "lodash",
},
{
accessor: ["moment"],
lib: moment,
version: moment.version,
docsURL: `https://momentjs.com/docs/`,
name: "moment",
},
{
accessor: ["xmlParser"],
lib: parser,
version: "3.17.5",
docsURL: "https://github.com/NaturalIntelligence/fast-xml-parser",
name: "xmlParser",
},
{
accessor: ["forge"],
// We are removing some functionalities of node-forge because they wont
// work in the worker thread
lib: _.omit(forge, ["tls", "http", "xhr", "socket", "task"]),
version: "1.3.0",
docsURL: "https://github.com/digitalbazaar/forge",
name: "forge",
},
];
export const JSLibraries = [...defaultLibraries];
export const libraryReservedIdentifiers = defaultLibraries.reduce(
(acc, lib) => {
lib.accessor.forEach((a) => (acc[a] = true));
return acc;
},
{} as Record<string, boolean>,
);
export function resetJSLibraries() {
JSLibraries.length = 0;
JSLibraries.push(...defaultLibraries);
const defaultLibraryAccessors = defaultLibraries.map(
(lib) => lib.accessor[0],
);
for (const key of Object.keys(libraryReservedIdentifiers)) {
if (defaultLibraryAccessors.includes(key)) continue;
try {
// @ts-expect-error: Types are not available
delete self[key];
} catch (e) {
// @ts-expect-error: Types are not available
self[key] = undefined;
}
delete libraryReservedIdentifiers[key];
}
return JSLibraries;
}

View File

@ -0,0 +1,84 @@
import log from "loglevel";
import { Def } from "tern";
function getTernDocType(obj: any) {
const type = typeof obj;
switch (type) {
case "string":
return "string";
case "number":
return "number";
case "boolean":
return "bool";
case "undefined":
return "?";
case "function":
return "fn()";
default:
return "?";
}
}
const ignoredKeys = [
"constructor",
"WINDOW",
"window",
"self",
"arguments",
"caller",
"length",
"name",
];
export function makeTernDefs(obj: any) {
const defs: Def = {};
const cachedDefs: any = [];
const visitedReferences: any = [];
const MAX_ITERATIONS = 5000;
let iteration_count = 1;
const baseObjPrototype = Object.getPrototypeOf({});
const queue = [[obj, defs]];
try {
while (queue.length && iteration_count < MAX_ITERATIONS) {
const [src, target] = queue.shift() as any;
if (visitedReferences.includes(src)) {
target["!type"] = cachedDefs[visitedReferences.indexOf(src)]["!type"];
continue;
}
const type = typeof src;
if (!src || (type !== "object" && type !== "function")) {
target["!type"] = getTernDocType(src);
continue;
} else if (type === "function") {
target["!type"] = "fn()";
}
queue.push(
...Object.getOwnPropertyNames(src)
.filter((key) => !ignoredKeys.includes(key))
.map((key) => {
target[key] = {};
return [src[key], target[key]];
}),
);
if (type === "object") {
const prototype = Object.getPrototypeOf(src);
if (prototype !== baseObjPrototype) {
queue.push(
...Object.getOwnPropertyNames(prototype)
.filter((key) => !ignoredKeys.includes(key))
.map((key) => {
target[key] = {};
return [src[key], target[key]];
}),
);
}
}
iteration_count++;
}
} catch (e) {
log.debug("Unknown depth", e);
}
return defs;
}

View File

@ -9,6 +9,5 @@ export enum WorkerErrorTypes {
export interface WorkerRequest<TData, TActions> {
method: TActions;
requestData: TData;
requestId: string;
data: TData;
}

View File

@ -28,6 +28,8 @@ import { watchJSActionSagas } from "sagas/JSActionSagas";
import selectionCanvasSagas from "../src/sagas/CanvasSagas/SelectionCanvasSagas";
import draggingCanvasSagas from "../src/sagas/CanvasSagas/DraggingCanvasSagas";
import formEvaluationChangeListener from "../src/sagas/FormEvaluationSaga";
import LintingSaga from "../src/sagas/LintingSagas";
import JSLibrarySaga from "../src/sagas/JSLibrarySaga";
export const sagasToRunForTests = [
initSagas,
@ -60,4 +62,6 @@ export const sagasToRunForTests = [
watchJSActionSagas,
selectionCanvasSagas,
draggingCanvasSagas,
LintingSaga,
JSLibrarySaga,
];

View File

@ -2,7 +2,7 @@
"extends": "./tsconfig.path.json",
"compilerOptions": {
"target": "ES6",
"lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include", "es2020.string", "esnext"],
"lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include", "es2020.string", "esnext", "WebWorker"],
"strict": true,
"outDir": "./out/js/src",
"allowJs": true,

View File

@ -5767,7 +5767,7 @@ css-select-base-adapter@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz"
css-select@4.1.3, css-select@^2.0.0, css-select@^4.1.3:
css-select@4.1.3, css-select@^2.0.0, css-select@^4.1.3, css-select@^5.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
@ -5904,6 +5904,11 @@ cssom@^0.4.4:
version "0.4.4"
resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz"
cssom@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
cssom@~0.3.6:
version "0.3.8"
resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz"
@ -6365,6 +6370,15 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0"
entities "^2.0.0"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
dom4@^2.1.5:
version "2.1.5"
resolved "https://registry.npmjs.org/dom4/-/dom4-2.1.5.tgz"
@ -6377,7 +6391,7 @@ domelementtype@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz"
domelementtype@^2.2.0:
domelementtype@^2.2.0, domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
@ -6401,6 +6415,13 @@ domhandler@^4.0.0, domhandler@^4.2.0:
dependencies:
domelementtype "^2.2.0"
domhandler@^5.0.1, domhandler@^5.0.2:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@1.5:
version "1.5.1"
resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz"
@ -6417,6 +6438,15 @@ domutils@^2.5.2, domutils@^2.6.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
domutils@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.1"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz"
@ -6611,6 +6641,11 @@ entities@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz"
entities@^4.2.0, entities@^4.3.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
errno@^0.1.3:
version "0.1.7"
resolved "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz"
@ -8255,6 +8290,11 @@ html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz"
html-escaper@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-3.0.3.tgz#4d336674652beb1dcbc29ef6b6ba7f6be6fdfed6"
integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==
html-minifier-terser@^6.0.2:
version "6.1.0"
resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
@ -8299,6 +8339,16 @@ htmlparser2@^6.1.0:
domutils "^2.5.2"
entities "^2.0.0"
htmlparser2@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010"
integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
domutils "^3.0.1"
entities "^4.3.0"
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz"
@ -9860,6 +9910,17 @@ lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
linkedom@^0.14.20:
version "0.14.20"
resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.20.tgz#4dd4418941507326ef7cc325e8e049853481dbb9"
integrity sha512-H7BX22kn4Ul4Mfr5/Jz039TgfsYce/YCvQ6272LEIlIJ1sYmU3R6yFNSYZU6iDX2aoF76wX+qjcSZEaLwumcAw==
dependencies:
css-select "^5.1.0"
cssom "^0.5.0"
html-escaper "^3.0.3"
htmlparser2 "^8.0.1"
uhyphen "^0.1.0"
lint-staged@^13.0.3:
version "13.0.3"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.0.3.tgz#d7cdf03a3830b327a2b63c6aec953d71d9dc48c6"
@ -14806,6 +14867,11 @@ uglify-js@^3.1.4:
version "3.13.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.4.tgz#592588bb9f47ae03b24916e2471218d914955574"
uhyphen@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/uhyphen/-/uhyphen-0.1.0.tgz#3cc22afa790daa802b9f6789f3583108d5b4a08c"
integrity sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"

View File

@ -5,14 +5,14 @@
$ cd app/client
$ yarn install myLibrary
```
2. In the file `app/client/src/utils/DynamicBindingUtils.ts` find the const `extraLibraries` and add details about your library in the codebase
2. In the file `app/client/src/worker/common/JSLibrary/index.ts` find the const `defaultLibraries` and add details about your library in the codebase
```
import myLibrary from "myLibrary";
...
...
const extraLibraries = [
const defaultLibraries = [
...
{
accessor: "myLibrary",// The namespace for access