diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24194_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24194_Spec.ts
new file mode 100644
index 0000000000..84c1fc5225
--- /dev/null
+++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24194_Spec.ts
@@ -0,0 +1,39 @@
+import {
+ agHelper,
+ entityExplorer,
+ jsEditor,
+ locators,
+ propPane,
+} from "../../../../support/Objects/ObjectsCore";
+
+describe("Responsiveness of linting", () => {
+ before(() => {
+ entityExplorer.DragDropWidgetNVerify("buttonwidget", 300, 300);
+ });
+ it("Should update linting when entity is added/renamed", () => {
+ const JS_OBJECT = `export default {
+ myFun1: () => {
+ return "";
+ },
+ myFun2: ()=>{
+ return ""
+ }
+ }`;
+ propPane.UpdatePropertyFieldValue("Tooltip", "{{JSObject1.myFun1}}");
+ agHelper.AssertElementExist(locators._lintErrorElement);
+ jsEditor.CreateJSObject(JS_OBJECT, {
+ paste: true,
+ completeReplace: true,
+ toRun: false,
+ shouldCreateNewJSObj: true,
+ });
+
+ entityExplorer.SelectEntityByName("Button1", "Widgets");
+ agHelper.AssertElementAbsence(locators._lintErrorElement);
+ agHelper.RefreshPage();
+ entityExplorer.SelectEntityByName("JSObject1", "Queries/JS");
+ jsEditor.RenameJSObjFromPane("JSObject2");
+ entityExplorer.SelectEntityByName("Button1", "Widgets");
+ agHelper.AssertElementAbsence(locators._lintErrorElement);
+ });
+});
diff --git a/app/client/cypress/e2e/Regression/ClientSide/Linting/BasicLint_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Linting/BasicLint_spec.ts
index 444e8885da..3bf237ab8a 100644
--- a/app/client/cypress/e2e/Regression/ClientSide/Linting/BasicLint_spec.ts
+++ b/app/client/cypress/e2e/Regression/ClientSide/Linting/BasicLint_spec.ts
@@ -366,4 +366,62 @@ describe("Linting", () => {
agHelper.AssertElementExist(locators._lintErrorElement);
},
);
+ it("10. Should not clear unrelated lint errors", () => {
+ const JS_OBJECT_WITH_MULTPLE_ERRORS = `export default {
+ myFun1: () => {
+ return error1;
+ },
+ myFun2: ()=>{
+ return error2
+ }
+ }`;
+ const JS_OBJECT_WITH_MYFUN2_EDITED = `export default {
+ myFun1: () => {
+ return error1;
+ },
+ myFun2: ()=>{
+ return "error cleared"
+ }
+ }`;
+
+ jsEditor.CreateJSObject(JS_OBJECT_WITH_MULTPLE_ERRORS, {
+ paste: true,
+ completeReplace: true,
+ toRun: false,
+ shouldCreateNewJSObj: true,
+ prettify: false,
+ });
+ agHelper.AssertElementExist(locators._lintErrorElement);
+
+ jsEditor.EditJSObj(JS_OBJECT_WITH_MYFUN2_EDITED, false);
+
+ agHelper.AssertElementExist(locators._lintErrorElement);
+ });
+ it("11. Shows correct lint error when js object has duplicate keys", () => {
+ const JS_OBJECT_WITH_DUPLICATE_KEYS = `export default {
+ myVar1: [],
+ myVar2: {},
+ myFun1 () {
+ // write code here
+ // this.myVar1 = [1,2,3]
+
+ },
+ async myFun1 () {
+ // use async-await or promises
+ // await storeValue('varName', 'hello world')
+ }
+ }`;
+
+ jsEditor.CreateJSObject(JS_OBJECT_WITH_DUPLICATE_KEYS, {
+ paste: true,
+ completeReplace: true,
+ toRun: false,
+ shouldCreateNewJSObj: true,
+ prettify: false,
+ });
+
+ agHelper
+ .AssertElementExist(locators._lintErrorElement)
+ .should("have.length", 1);
+ });
});
diff --git a/app/client/cypress/e2e/Regression/ClientSide/Performance/LinkRelPreload_Spec.js b/app/client/cypress/e2e/Regression/ClientSide/Performance/LinkRelPreload_Spec.js
index 6522cc0dad..d90ac1ea2a 100644
--- a/app/client/cypress/e2e/Regression/ClientSide/Performance/LinkRelPreload_Spec.js
+++ b/app/client/cypress/e2e/Regression/ClientSide/Performance/LinkRelPreload_Spec.js
@@ -27,30 +27,30 @@ import emptyDSL from "../../../../fixtures/emptyDSL.json";
// started failing for you, it’s likely you import()ed some new chunks that the edit or the view mode uses.
// To fix the test, see preloading instructions in public/index.html.
-describe("html should include s for all code-split javascript", function () {
+describe("html should include preload metadata for all code-split javascript", function () {
before(() => {
cy.addDsl(emptyDSL);
});
- it("1. In edit & View mode", function () {
- testLinkRelPreloads("edit-mode");
+ it("1. In edit mode", function () {
+ testPreloadMetadata("edit-mode");
});
// Note: this must be a separate test from the previous one,
// as we’re relying on Cypress resetting intercepts between tests.
it("2. In view mode", function () {
- cy.reload();
+ reloadAndTogglePreloading(true);
// Ensure the app editor is fully loaded
cy.get("#sidebar").should("be.visible");
_.deployMode.DeployApp();
- testLinkRelPreloads("view-mode");
+ testPreloadMetadata("view-mode");
});
});
-function testLinkRelPreloads(viewOrEditMode) {
+function testPreloadMetadata(viewOrEditMode) {
// Disable network caching in Chromium, per https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
// and https://github.com/cypress-io/cypress/issues/14459#issuecomment-768616195
Cypress.automation("remote:debugger:protocol", {
@@ -66,7 +66,7 @@ function testLinkRelPreloads(viewOrEditMode) {
// Intercept all JS network requests and collect them
cy.intercept(/\/static\/js\/.+\.js/, (req) => {
- // Ignore
+ // Don’t collect:
// - requests to worker files
// - requests to icons
// - request to the main bundle
@@ -83,11 +83,11 @@ function testLinkRelPreloads(viewOrEditMode) {
// Make all web workers empty. This prevents web workers from loading additional chunks,
// as we need to collect only chunks from the main thread
- cy.intercept(/\/static\/js\/.+Worker\..+\.js/, { body: "" }).as(
- "workerRequests",
- );
+ cy.intercept(/\/static\/js\/.+Worker\..+\.js/, { body: "" }).as("worker");
- cy.reload();
+ // Reload without preloading, as we want to collect only chunks
+ // actually requested by the current route
+ reloadAndTogglePreloading(false);
cy.waitForNetworkIdle("/static/js/*.js", 5000, { timeout: 60 * 1000 });
@@ -127,16 +127,16 @@ function testLinkRelPreloads(viewOrEditMode) {
),
);
- const requestsString = `[${
+ const actuallyLoadedFiles = `[${
requestsToCompare.length
} items] ${requestsToCompare.sort().join(", ")}`;
- const linksString = `[${linksToCompare.length} items] ${linksToCompare
+ const preloadedFiles = `[${linksToCompare.length} items] ${linksToCompare
.sort()
.join(", ")}`;
// Comparing strings instead of deep-equalling arrays because this is the only way
// to see which chunks are actually missing: https://github.com/cypress-io/cypress/issues/4084
- cy.wrap(requestsString).should("equal", linksString);
+ cy.wrap(actuallyLoadedFiles).should("equal", preloadedFiles);
});
}
@@ -144,3 +144,15 @@ function testLinkRelPreloads(viewOrEditMode) {
function unique(arr) {
return Array.from(new Set(arr));
}
+
+function reloadAndTogglePreloading(chunkPreloadingEnabled) {
+ cy.url().then((currentURL) => {
+ let url = new URL(currentURL);
+ if (chunkPreloadingEnabled) {
+ url.searchParams.set("disableChunkPreload", "true");
+ } else {
+ url.searchParams.delete("disableChunkPreload");
+ }
+ cy.visit(url.toString());
+ });
+}
diff --git a/app/client/packages/ast/index.ts b/app/client/packages/ast/index.ts
index 36ad64746c..10be540aa3 100644
--- a/app/client/packages/ast/index.ts
+++ b/app/client/packages/ast/index.ts
@@ -25,7 +25,12 @@ import {
import { ECMA_VERSION, SourceType, NodeTypes } from "./src/constants";
// JSObjects
-import type { TParsedJSProperty, JSPropertyPosition } from "./src/jsObject";
+import type {
+ TParsedJSProperty,
+ JSPropertyPosition,
+ JSVarProperty,
+ JSFunctionProperty,
+} from "./src/jsObject";
import { parseJSObject, isJSFunctionProperty } from "./src/jsObject";
// action creator
@@ -73,6 +78,8 @@ export type {
TParsedJSProperty,
JSPropertyPosition,
PeekOverlayExpressionIdentifierOptions,
+ JSVarProperty,
+ JSFunctionProperty,
};
export {
diff --git a/app/client/packages/ast/src/jsObject/index.ts b/app/client/packages/ast/src/jsObject/index.ts
index c66c7d91f9..5b819396ba 100644
--- a/app/client/packages/ast/src/jsObject/index.ts
+++ b/app/client/packages/ast/src/jsObject/index.ts
@@ -35,7 +35,7 @@ export interface JSPropertyPosition {
keyEndColumn: number;
}
-interface baseJSProperty {
+interface BaseJSProperty {
key: string;
value: string;
type: string;
@@ -43,12 +43,12 @@ interface baseJSProperty {
rawContent: string;
}
-type JSFunctionProperty = baseJSProperty & {
+export type JSFunctionProperty = BaseJSProperty & {
arguments: functionParam[];
// If function uses the "async" keyword
isMarkedAsync: boolean;
};
-type JSVarProperty = baseJSProperty;
+export type JSVarProperty = BaseJSProperty;
export type TParsedJSProperty = JSVarProperty | JSFunctionProperty;
diff --git a/app/client/src/actions/evaluationActions.ts b/app/client/src/actions/evaluationActions.ts
index 8bae2e5a45..a205914c47 100644
--- a/app/client/src/actions/evaluationActions.ts
+++ b/app/client/src/actions/evaluationActions.ts
@@ -3,7 +3,7 @@ import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
-import _ from "lodash";
+import { intersection, union } from "lodash";
import type { DataTree } from "entities/DataTree/dataTreeFactory";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { Diff } from "deep-diff";
@@ -30,8 +30,14 @@ export const LINT_REDUX_ACTIONS = {
[ReduxActionTypes.UPDATE_LAYOUT]: true,
[ReduxActionTypes.UPDATE_WIDGET_PROPERTY]: true,
[ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS]: true,
- [ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS]: true,
+ [ReduxActionTypes.UPDATE_JS_ACTION_BODY_INIT]: true, // "lint only" action
[ReduxActionTypes.META_UPDATE_DEBOUNCED_EVAL]: true,
+ [ReduxActionTypes.FETCH_JS_ACTIONS_FOR_PAGE_SUCCESS]: true,
+ [ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS]: true,
+ [ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true,
+ [ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS]: true,
+ [ReduxActionTypes.BUFFERED_ACTION]: true,
+ [ReduxActionTypes.BATCH_UPDATES_SUCCESS]: true,
};
export const LOG_REDUX_ACTIONS = {
@@ -94,41 +100,46 @@ export const EVALUATE_REDUX_ACTIONS = [
ReduxActionTypes.UPDATE_SELECTED_APP_THEME_SUCCESS,
ReduxActionTypes.CHANGE_SELECTED_APP_THEME_SUCCESS,
ReduxActionTypes.SET_PREVIEW_APP_THEME,
+
+ // Custom Library
+ ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
+ ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS,
+ // Buffer
+ ReduxActionTypes.BUFFERED_ACTION,
];
// Topics used for datasource and query form evaluations
export const FORM_EVALUATION_REDUX_ACTIONS = [
ReduxActionTypes.INIT_FORM_EVALUATION,
ReduxActionTypes.RUN_FORM_EVALUATION,
];
-export const shouldProcessBatchedAction = (action: ReduxAction) => {
- if (
- action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
- Array.isArray(action.payload)
- ) {
- const batchedActionTypes = action.payload.map(
- (batchedAction) => batchedAction.type,
- );
- return (
- _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0
- );
- }
- return true;
+
+export const shouldTriggerEvaluation = (action: ReduxAction) => {
+ return (
+ shouldProcessAction(action) && EVALUATE_REDUX_ACTIONS.includes(action.type)
+ );
+};
+export const shouldTriggerLinting = (action: ReduxAction) => {
+ return shouldProcessAction(action) && !!LINT_REDUX_ACTIONS[action.type];
};
-export function shouldLint(action: ReduxAction) {
+export const getAllActionTypes = (action: ReduxAction) => {
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
const batchedActionTypes = action.payload.map(
- (batchedAction) => batchedAction.type,
- );
- return batchedActionTypes.some(
- (actionType) => LINT_REDUX_ACTIONS[actionType],
+ (batchedAction) => batchedAction.type as string,
);
+ return batchedActionTypes;
}
- return LINT_REDUX_ACTIONS[action.type];
-}
+ return [action.type];
+};
+
+export const shouldProcessAction = (action: ReduxAction) => {
+ const actionTypes = getAllActionTypes(action);
+
+ return intersection(EVAL_AND_LINT_REDUX_ACTIONS, actionTypes).length > 0;
+};
export function shouldLog(action: ReduxAction) {
if (
@@ -199,3 +210,18 @@ export const startFormEvaluations = (
},
};
};
+
+// These actions require the entire tree to be re-evaluated
+const FORCE_EVAL_ACTIONS = {
+ [ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true,
+ [ReduxActionTypes.UNINSTALL_LIBRARY_SUCCESS]: true,
+};
+
+export const shouldForceEval = (action: ReduxAction) => {
+ return !!FORCE_EVAL_ACTIONS[action.type];
+};
+
+export const EVAL_AND_LINT_REDUX_ACTIONS = union(
+ EVALUATE_REDUX_ACTIONS,
+ Object.keys(LINT_REDUX_ACTIONS),
+);
diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx
index 20eb9cdf2b..981c9d6351 100644
--- a/app/client/src/ce/constants/ReduxActionConstants.tsx
+++ b/app/client/src/ce/constants/ReduxActionConstants.tsx
@@ -839,6 +839,7 @@ const ActionTypes = {
DATASOURCE_DISCARD_ACTION: "DATASOURCE_DISCARD_ACTION",
SET_ONE_CLICK_BINDING_OPTIONS_VISIBILITY:
"SET_ONE_CLICK_BINDING_OPTIONS_VISIBILITY",
+ BUFFERED_ACTION: "BUFFERED_ACTION",
};
export const ReduxActionTypes = {
diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts
index 1e66ce44f6..490168201d 100644
--- a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts
+++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts
@@ -95,7 +95,7 @@ export function getEntityNameAndPropertyPath(fullPath: string): {
return { entityName, propertyPath };
}
-function translateCollectionDiffs(
+export function translateCollectionDiffs(
propertyPath: string,
data: unknown,
event: DataTreeDiffEvent,
@@ -664,31 +664,31 @@ export const isDynamicLeaf = (
};
export const addWidgetPropertyDependencies = ({
- entity,
- entityName,
+ widgetConfig,
+ widgetName,
}: {
- entity: WidgetEntityConfig;
- entityName: string;
+ widgetConfig: WidgetEntityConfig;
+ widgetName: string;
}) => {
const dependencies: DependencyMap = {};
- Object.entries(entity.propertyOverrideDependency).forEach(
+ Object.entries(widgetConfig.propertyOverrideDependency).forEach(
([overriddenPropertyKey, overridingPropertyKeyMap]) => {
const existingDependenciesSet = new Set(
- dependencies[`${entityName}.${overriddenPropertyKey}`] || [],
+ dependencies[`${widgetName}.${overriddenPropertyKey}`] || [],
);
// add meta dependency
overridingPropertyKeyMap.META &&
existingDependenciesSet.add(
- `${entityName}.${overridingPropertyKeyMap.META}`,
+ `${widgetName}.${overridingPropertyKeyMap.META}`,
);
// add default dependency
overridingPropertyKeyMap.DEFAULT &&
existingDependenciesSet.add(
- `${entityName}.${overridingPropertyKeyMap.DEFAULT}`,
+ `${widgetName}.${overridingPropertyKeyMap.DEFAULT}`,
);
- dependencies[`${entityName}.${overriddenPropertyKey}`] = [
+ dependencies[`${widgetName}.${overriddenPropertyKey}`] = [
...existingDependenciesSet,
];
},
diff --git a/app/client/src/components/editorComponents/CodeEditor/constants.ts b/app/client/src/components/editorComponents/CodeEditor/constants.ts
index a33d98c86b..4bbb1b07b3 100644
--- a/app/client/src/components/editorComponents/CodeEditor/constants.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/constants.ts
@@ -1,5 +1,5 @@
+import { JS_OBJECT_START_STATEMENT } from "plugins/Linting/constants";
import type { Position } from "codemirror";
-import { JS_OBJECT_START_STATEMENT } from "workers/Linting/constants";
export const LINT_TOOLTIP_CLASS = "CodeMirror-lint-tooltip";
export const LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS = "CodeMirror-lint-tooltip-left";
diff --git a/app/client/src/components/editorComponents/CodeEditor/lintHelpers.test.ts b/app/client/src/components/editorComponents/CodeEditor/lintHelpers.test.ts
index 4151e8bdd5..a9cae8af4f 100644
--- a/app/client/src/components/editorComponents/CodeEditor/lintHelpers.test.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/lintHelpers.test.ts
@@ -4,7 +4,7 @@ import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
import {
INVALID_JSOBJECT_START_STATEMENT,
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
-} from "workers/Linting/constants";
+} from "plugins/Linting/constants";
import { CODE_EDITOR_START_POSITION } from "./constants";
import {
getKeyPositionInString,
diff --git a/app/client/src/components/editorComponents/CodeEditor/lintHelpers.ts b/app/client/src/components/editorComponents/CodeEditor/lintHelpers.ts
index 1b33c90012..a6080d4e5b 100644
--- a/app/client/src/components/editorComponents/CodeEditor/lintHelpers.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/lintHelpers.ts
@@ -14,7 +14,7 @@ import {
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
INVALID_JSOBJECT_START_STATEMENT,
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
-} from "workers/Linting/constants";
+} from "plugins/Linting/constants";
export const getIndexOfRegex = (
str: string,
regex: RegExp,
diff --git a/app/client/src/components/editorComponents/LazyCodeEditor/CodeEditorFallback.tsx b/app/client/src/components/editorComponents/LazyCodeEditor/CodeEditorFallback.tsx
index 9f63c05cdc..eaadb60755 100644
--- a/app/client/src/components/editorComponents/LazyCodeEditor/CodeEditorFallback.tsx
+++ b/app/client/src/components/editorComponents/LazyCodeEditor/CodeEditorFallback.tsx
@@ -9,7 +9,7 @@ import {
import { ContentKind } from "./types";
import type { EditorProps } from "components/editorComponents/CodeEditor";
import { Spinner } from "design-system";
-import { JS_OBJECT_START_STATEMENT } from "workers/Linting/constants";
+import { JS_OBJECT_START_STATEMENT } from "plugins/Linting/constants";
export default function CodeEditorFallback({
input,
diff --git a/app/client/src/entities/Action/actionProperties.ts b/app/client/src/entities/Action/actionProperties.ts
index 75eb54c4d4..51f5f8b28d 100644
--- a/app/client/src/entities/Action/actionProperties.ts
+++ b/app/client/src/entities/Action/actionProperties.ts
@@ -16,6 +16,7 @@ import {
import formControlTypes from "utils/formControl/formControlTypes";
import { getAllBindingPathsForGraphqlPagination } from "utils/editor/EditorBindingPaths";
import EditorControlTypes from "utils/editor/EditorControlTypes";
+import type { DynamicPath } from "utils/DynamicBindingUtils";
const dynamicFields = [
formControlTypes.QUERY_DYNAMIC_TEXT,
@@ -38,6 +39,7 @@ const getCorrectEvaluationSubstitutionType = (substitutionType?: string) => {
export const getBindingAndReactivePathsOfAction = (
action: Action,
formConfig?: any[],
+ dynamicBindingPathList?: DynamicPath[],
): { reactivePaths: ReactivePaths; bindingPaths: BindingPaths } => {
let reactivePaths: ReactivePaths = {
data: EvaluationSubstitutionType.TEMPLATE,
@@ -46,6 +48,9 @@ export const getBindingAndReactivePathsOfAction = (
};
const bindingPaths: BindingPaths = {};
if (!formConfig) {
+ dynamicBindingPathList?.forEach((dynamicPath) => {
+ reactivePaths[dynamicPath.key] = EvaluationSubstitutionType.TEMPLATE;
+ });
reactivePaths = {
...reactivePaths,
config: EvaluationSubstitutionType.TEMPLATE,
diff --git a/app/client/src/entities/DataTree/dataTreeAction.ts b/app/client/src/entities/DataTree/dataTreeAction.ts
index b04e9db789..d9f40f2d42 100644
--- a/app/client/src/entities/DataTree/dataTreeAction.ts
+++ b/app/client/src/entities/DataTree/dataTreeAction.ts
@@ -46,6 +46,7 @@ export const generateDataTreeAction = (
const { bindingPaths, reactivePaths } = getBindingAndReactivePathsOfAction(
action.config,
editorConfig,
+ dynamicBindingPathList,
);
return {
diff --git a/app/client/src/entities/DataTree/types.ts b/app/client/src/entities/DataTree/types.ts
index afe60fdc71..0fd64a8006 100644
--- a/app/client/src/entities/DataTree/types.ts
+++ b/app/client/src/entities/DataTree/types.ts
@@ -5,6 +5,7 @@ import type { ActionConfig, PluginType } from "entities/Action";
import type { ActionDescription } from "@appsmith/workers/Evaluation/fns";
import type { Variable } from "entities/JSCollection";
import type { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils";
+import type { Page } from "@appsmith/constants/ReduxActionConstants";
export type ActionDispatcher = (...args: any[]) => ActionDescription;
@@ -78,6 +79,7 @@ export interface JSActionEntity {
ENTITY_TYPE: ENTITY_TYPE.JSACTION;
actionId: string;
}
+export type PagelistEntity = Page[];
// Widget entity Types
diff --git a/app/client/src/entities/DependencyMap/__tests__/index.test.ts b/app/client/src/entities/DependencyMap/__tests__/index.test.ts
index 4bb0f96402..33ad9c63ba 100644
--- a/app/client/src/entities/DependencyMap/__tests__/index.test.ts
+++ b/app/client/src/entities/DependencyMap/__tests__/index.test.ts
@@ -36,7 +36,7 @@ describe("Tests for DependencyMap", () => {
dataDependencyMap.addNodes({ showAlert: true });
dataDependencyMap.addDependency("c", ["showAlert"]);
- expect(dataDependencyMap.isRelated("a", "showAlert")).toEqual(true);
+ expect(dataDependencyMap.isRelated("a", ["showAlert"])).toEqual(true);
});
it("should be able to remove a node", () => {
diff --git a/app/client/src/entities/DependencyMap/index.ts b/app/client/src/entities/DependencyMap/index.ts
index 881f87d1d4..552b88bb40 100644
--- a/app/client/src/entities/DependencyMap/index.ts
+++ b/app/client/src/entities/DependencyMap/index.ts
@@ -1,9 +1,10 @@
+export type TDependencies = Map>;
export default class DependencyMap {
#nodes: Map;
- #dependencies: Map>;
- #dependenciesInverse: Map>;
- #invalidDependencies: Map>;
- #invalidDependenciesInverse: Map>;
+ #dependencies: TDependencies;
+ #dependenciesInverse: TDependencies;
+ #invalidDependencies: TDependencies;
+ #invalidDependenciesInverse: TDependencies;
constructor() {
this.#nodes = new Map();
@@ -59,6 +60,16 @@ export default class DependencyMap {
public addDependency = (node: string, dependencies: string[]) => {
const validDependencies = new Set();
const invalidDependencies = new Set();
+
+ const currentNodeDependencies =
+ this.#dependencies.get(node) || new Set();
+
+ for (const currentDependency of currentNodeDependencies) {
+ if (!dependencies.includes(currentDependency)) {
+ this.#dependenciesInverse.get(currentDependency)?.delete(node);
+ }
+ }
+
for (const dependency of dependencies) {
if (this.#nodes.has(dependency)) {
validDependencies.add(dependency);
@@ -139,15 +150,15 @@ export default class DependencyMap {
}
};
- isRelated = (source: string, target: string) => {
- if (source === target) return true;
+ isRelated = (source: string, targets: string[]) => {
+ if (targets.includes(source)) return true;
const visited = new Set();
const queue = [source];
while (queue.length) {
const node = queue.shift() as string;
if (visited.has(node)) continue;
visited.add(node);
- if (node === target) return true;
+ if (targets.includes(node)) return true;
const nodes = this.#dependencies.get(node) || [];
for (const n of nodes) {
queue.push(n);
@@ -155,4 +166,31 @@ export default class DependencyMap {
}
return false;
};
+
+ getDependents(node: string) {
+ const nodes = this.#dependenciesInverse.get(node);
+ return Array.from(nodes || []);
+ }
+ getDirectDependencies(node: string) {
+ const nodes = this.#dependencies.get(node);
+ return Array.from(nodes || []);
+ }
+
+ getAllReachableNodes(source: string, targets: string[]) {
+ const reachableNodes: string[] = [];
+ if (targets.includes(source)) reachableNodes.push(source);
+ const visited = new Set();
+ const queue = [source];
+ while (queue.length) {
+ const node = queue.shift() as string;
+ if (visited.has(node)) continue;
+ visited.add(node);
+ if (targets.includes(node)) reachableNodes.push(source);
+ const nodes = this.#dependencies.get(node) || [];
+ for (const n of nodes) {
+ queue.push(n);
+ }
+ }
+ return reachableNodes;
+ }
}
diff --git a/app/client/src/plugins/Linting/Linter.ts b/app/client/src/plugins/Linting/Linter.ts
new file mode 100644
index 0000000000..e4b5936d9a
--- /dev/null
+++ b/app/client/src/plugins/Linting/Linter.ts
@@ -0,0 +1,26 @@
+import type { ILinter } from "./linters";
+import { BaseLinter, WorkerLinter } from "./linters";
+import type { LintTreeRequestPayload, updateJSLibraryProps } from "./types";
+
+export class Linter {
+ linter: ILinter;
+ constructor(options: { useWorker: boolean }) {
+ this.linter = options.useWorker ? new WorkerLinter() : new BaseLinter();
+ this.lintTree = this.lintTree.bind(this);
+ this.updateJSLibraryGlobals = this.updateJSLibraryGlobals.bind(this);
+ this.start = this.start.bind(this);
+ this.shutdown = this.shutdown.bind(this);
+ }
+ *lintTree(data: LintTreeRequestPayload) {
+ return yield* this.linter.lintTree(data);
+ }
+ *updateJSLibraryGlobals(data: updateJSLibraryProps) {
+ return yield* this.linter.updateJSLibraryGlobals(data);
+ }
+ *start() {
+ yield this.linter.start();
+ }
+ *shutdown() {
+ yield this.linter.shutdown();
+ }
+}
diff --git a/app/client/src/workers/Linting/constants.ts b/app/client/src/plugins/Linting/constants.ts
similarity index 100%
rename from app/client/src/workers/Linting/constants.ts
rename to app/client/src/plugins/Linting/constants.ts
diff --git a/app/client/src/workers/Linting/globalData.ts b/app/client/src/plugins/Linting/globalData.ts
similarity index 100%
rename from app/client/src/workers/Linting/globalData.ts
rename to app/client/src/plugins/Linting/globalData.ts
diff --git a/app/client/src/plugins/Linting/handlers/index.ts b/app/client/src/plugins/Linting/handlers/index.ts
new file mode 100644
index 0000000000..3db9823e8b
--- /dev/null
+++ b/app/client/src/plugins/Linting/handlers/index.ts
@@ -0,0 +1,8 @@
+import { LINT_WORKER_ACTIONS } from "plugins/Linting/types";
+import { updateJSLibraryGlobals } from "./updateJSLibraryGlobals";
+import { lintService } from "./lintService";
+
+export const handlerMap = {
+ [LINT_WORKER_ACTIONS.LINT_TREE]: lintService.lintTree,
+ [LINT_WORKER_ACTIONS.UPDATE_LINT_GLOBALS]: updateJSLibraryGlobals,
+} as const;
diff --git a/app/client/src/plugins/Linting/handlers/lintService.ts b/app/client/src/plugins/Linting/handlers/lintService.ts
new file mode 100644
index 0000000000..9eb7c8913f
--- /dev/null
+++ b/app/client/src/plugins/Linting/handlers/lintService.ts
@@ -0,0 +1,352 @@
+import { get, intersection, isEmpty, uniq } from "lodash";
+import {
+ convertPathToString,
+ getAllPaths,
+ getEntityNameAndPropertyPath,
+} from "@appsmith/workers/Evaluation/evaluationUtils";
+import { AppsmithFunctionsWithFields } from "components/editorComponents/ActionCreator/constants";
+import { PathUtils } from "plugins/Linting/utils/pathUtils";
+import { extractReferencesFromPath } from "plugins/Linting/utils/getEntityDependencies";
+import { groupDifferencesByType } from "plugins/Linting/utils/groupDifferencesByType";
+import type {
+ LintTreeRequestPayload,
+ LintTreeResponse,
+} from "plugins/Linting/types";
+import { getLintErrorsFromTree } from "plugins/Linting/lintTree";
+import type {
+ TJSPropertiesState,
+ TJSpropertyState,
+} from "workers/Evaluation/JSObject/jsPropertiesState";
+import { isJSEntity } from "plugins/Linting/lib/entity";
+import DependencyMap from "entities/DependencyMap";
+import {
+ LintEntityTree,
+ type EntityTree,
+} from "plugins/Linting/lib/entity/EntityTree";
+import { entityFns } from "workers/Evaluation/fns";
+
+class LintService {
+ cachedEntityTree: EntityTree | null;
+ dependencyMap: DependencyMap = new DependencyMap();
+ constructor() {
+ this.cachedEntityTree = null;
+ if (isEmpty(this.cachedEntityTree)) {
+ this.dependencyMap = new DependencyMap();
+ this.dependencyMap.addNodes(
+ convertArrayToObject(AppsmithFunctionsWithFields),
+ );
+ }
+ }
+
+ lintTree = (payload: LintTreeRequestPayload) => {
+ const {
+ cloudHosting,
+ configTree,
+ forceLinting = false,
+ unevalTree: unEvalTree,
+ } = payload;
+
+ const entityTree = new LintEntityTree(unEvalTree, configTree);
+
+ const { asyncJSFunctionsInDataFields, pathsToLint } =
+ isEmpty(this.cachedEntityTree) || forceLinting
+ ? this.lintFirstTree(entityTree)
+ : this.lintUpdatedTree(entityTree);
+
+ const jsEntities = entityTree.getEntities().filter(isJSEntity);
+ const jsPropertiesState: TJSPropertiesState = {};
+ for (const jsEntity of jsEntities) {
+ const rawEntity = jsEntity.getRawEntity();
+ const config = jsEntity.getConfig();
+ if (!jsEntity.entityParser) continue;
+ const { parsedEntityConfig } = jsEntity.entityParser.parse(
+ rawEntity,
+ config,
+ );
+ jsPropertiesState[jsEntity.getName()] = parsedEntityConfig as Record<
+ string,
+ TJSpropertyState
+ >;
+ }
+
+ const lintTreeResponse: LintTreeResponse = {
+ errors: {},
+ lintedJSPaths: [],
+ jsPropertiesState,
+ };
+ try {
+ const { errors: lintErrors, lintedJSPaths } = getLintErrorsFromTree({
+ pathsToLint,
+ unEvalTree: this.cachedEntityTree?.getRawTree() || {},
+ jsPropertiesState,
+ cloudHosting,
+ asyncJSFunctionsInDataFields,
+
+ configTree,
+ });
+
+ lintTreeResponse.errors = lintErrors;
+ lintTreeResponse.lintedJSPaths = lintedJSPaths;
+ } catch (e) {}
+ return lintTreeResponse;
+ };
+
+ private lintFirstTree = (entityTree: EntityTree) => {
+ const pathsToLint: Array = [];
+ const allNodes: Record = entityTree.getAllPaths();
+ const asyncJSFunctionsInDataFields: Record = {};
+ this.dependencyMap.addNodes(allNodes);
+
+ const entities = entityTree.getEntities();
+
+ for (const entity of entities) {
+ const dynamicPaths = PathUtils.getDynamicPaths(entity);
+ for (const path of dynamicPaths) {
+ const references = extractReferencesFromPath(entity, path, allNodes);
+ this.dependencyMap.addDependency(path, references);
+ pathsToLint.push(path);
+ }
+ }
+ const asyncEntityActions = AppsmithFunctionsWithFields.concat(
+ getAllEntityActions(entityTree),
+ );
+ const asyncFns = entities
+ .filter(isJSEntity)
+ .flatMap((e) => e.getFns())
+ .filter(
+ (fn) =>
+ fn.isMarkedAsync ||
+ this.dependencyMap.isRelated(fn.name, asyncEntityActions),
+ )
+ .map((fn) => fn.name);
+
+ for (const asyncFn of asyncFns) {
+ const nodesThatDependOnAsyncFn =
+ this.dependencyMap.getDependents(asyncFn);
+ const dataPathsThatDependOnAsyncFn = filterDataPaths(
+ nodesThatDependOnAsyncFn,
+ entityTree,
+ );
+ if (isEmpty(dataPathsThatDependOnAsyncFn)) continue;
+ asyncJSFunctionsInDataFields[asyncFn] = dataPathsThatDependOnAsyncFn;
+ }
+
+ this.cachedEntityTree = entityTree;
+ return {
+ pathsToLint,
+ asyncJSFunctionsInDataFields,
+ };
+ };
+
+ private lintUpdatedTree(entityTree: EntityTree) {
+ const asyncJSFunctionsInDataFields: Record = {};
+ const pathsToLint: string[] = [];
+ const NOOP = {
+ pathsToLint: [],
+ asyncJSFunctionsInDataFields,
+ };
+ const entityTreeDiff =
+ this.cachedEntityTree?.computeDifferences(entityTree);
+ if (!entityTreeDiff) return NOOP;
+
+ const { additions, deletions, edits } =
+ groupDifferencesByType(entityTreeDiff);
+
+ const allNodes = getAllPaths(entityTree.getRawTree());
+
+ const updatedPathsDetails: Record<
+ string,
+ {
+ previousDependencies: string[];
+ currentDependencies: string[];
+ updateType: "EDIT" | "ADD" | "DELETE";
+ }
+ > = {};
+
+ for (const edit of edits) {
+ const pathString = convertPathToString(edit?.path || []);
+ if (!pathString) continue;
+ const { entityName } = getEntityNameAndPropertyPath(pathString);
+ const entity = entityTree.getEntityByName(entityName);
+ if (!entity) continue;
+ const dynamicPaths = PathUtils.getDynamicPaths(entity);
+ if (!dynamicPaths.includes(pathString)) {
+ if (!dynamicPaths.some((p) => pathString.startsWith(p))) continue;
+ }
+
+ const previousDependencies =
+ this.dependencyMap.getDirectDependencies(pathString);
+ const references = extractReferencesFromPath(
+ entity,
+ pathString,
+ allNodes,
+ );
+ this.dependencyMap.addDependency(pathString, references);
+ pathsToLint.push(pathString);
+
+ updatedPathsDetails[pathString] = {
+ previousDependencies,
+ currentDependencies: references,
+ updateType: "EDIT",
+ };
+ }
+
+ for (const addition of additions) {
+ const pathString = convertPathToString(addition?.path || []);
+ if (!pathString) continue;
+ const { entityName } = getEntityNameAndPropertyPath(pathString);
+ if (!entityName) continue;
+ const entity = entityTree.getEntityByName(entityName);
+ if (!entity) continue;
+ const allAddedPaths = PathUtils.getAllPaths({
+ [pathString]: get(entityTree.getRawTree(), pathString),
+ });
+ this.dependencyMap.addNodes(allAddedPaths);
+ for (const path of Object.keys(allAddedPaths)) {
+ const previousDependencies =
+ this.dependencyMap.getDirectDependencies(path);
+ const references = extractReferencesFromPath(entity, path, allNodes);
+ if (PathUtils.isDynamicLeaf(entity, path)) {
+ this.dependencyMap.addDependency(path, references);
+ pathsToLint.push(path);
+ }
+ const incomingDeps = this.dependencyMap.getDependents(path);
+ pathsToLint.push(...incomingDeps);
+
+ updatedPathsDetails[path] = {
+ previousDependencies,
+ currentDependencies: references,
+ updateType: "ADD",
+ };
+ }
+ }
+ for (const deletion of deletions) {
+ const pathString = convertPathToString(deletion?.path || []);
+ if (!pathString) continue;
+ const { entityName } = getEntityNameAndPropertyPath(pathString);
+ if (!entityName) continue;
+ const entity = this.cachedEntityTree?.getEntityByName(entityName); // Use previous tree in a DELETE EVENT
+ if (!entity) continue;
+
+ const allDeletedPaths = PathUtils.getAllPaths({
+ [pathString]: get(this.cachedEntityTree?.getRawTree(), pathString),
+ });
+
+ for (const path of Object.keys(allDeletedPaths)) {
+ const previousDependencies =
+ this.dependencyMap.getDirectDependencies(path);
+
+ updatedPathsDetails[path] = {
+ previousDependencies,
+ currentDependencies: [],
+ updateType: "DELETE",
+ };
+
+ const incomingDeps = this.dependencyMap.getDependents(path);
+ pathsToLint.push(...incomingDeps);
+ }
+ this.dependencyMap.removeNodes(allDeletedPaths);
+ }
+
+ // generate async functions only after dependencyMap update is complete
+ const asyncEntityActions = AppsmithFunctionsWithFields.concat(
+ getAllEntityActions(entityTree),
+ );
+ const asyncFns = entityTree
+ .getEntities()
+ .filter(isJSEntity)
+ .flatMap((e) => e.getFns())
+ .filter(
+ (fn) =>
+ fn.isMarkedAsync ||
+ this.dependencyMap.isRelated(fn.name, asyncEntityActions),
+ )
+ .map((fn) => fn.name);
+
+ // generate asyncFunctionsBoundToSyncFields
+
+ for (const [updatedPath, details] of Object.entries(updatedPathsDetails)) {
+ const { currentDependencies, previousDependencies, updateType } = details;
+ const { entityName } = getEntityNameAndPropertyPath(updatedPath);
+ if (!entityName) continue;
+ // Use cached entityTree in a delete event
+ const entityTreeToUse =
+ updateType === "DELETE" ? this.cachedEntityTree : entityTree;
+ const entity = entityTreeToUse?.getEntityByName(entityName);
+ if (!entity) continue;
+
+ if (isJSEntity(entity) && asyncFns.includes(updatedPath)) {
+ const nodesThatDependOnAsyncFn =
+ this.dependencyMap.getDependents(updatedPath);
+ const dataPathsThatDependOnAsyncFn = filterDataPaths(
+ nodesThatDependOnAsyncFn,
+ entityTree,
+ );
+ if (!isEmpty(dataPathsThatDependOnAsyncFn)) {
+ asyncJSFunctionsInDataFields[updatedPath] =
+ dataPathsThatDependOnAsyncFn;
+ }
+ continue;
+ }
+
+ const isDataPath = PathUtils.isDataPath(updatedPath, entity);
+ if (!isDataPath) continue;
+
+ const asyncDeps = intersection(asyncFns, currentDependencies);
+ const prevAsyncDeps = intersection(asyncFns, previousDependencies);
+
+ for (const asyncFn of asyncDeps) {
+ const nodesThatDependOnAsyncFn =
+ this.dependencyMap.getDependents(asyncFn);
+ const dataPathsThatDependOnAsyncFn = filterDataPaths(
+ nodesThatDependOnAsyncFn,
+ entityTree,
+ );
+ if (isEmpty(dataPathsThatDependOnAsyncFn)) continue;
+ asyncJSFunctionsInDataFields[asyncFn] = dataPathsThatDependOnAsyncFn;
+ }
+ pathsToLint.push(...asyncDeps, ...prevAsyncDeps);
+ }
+
+ this.cachedEntityTree = entityTree;
+ return {
+ pathsToLint: uniq(pathsToLint),
+ entityTree,
+ asyncJSFunctionsInDataFields,
+ };
+ }
+}
+
+function convertArrayToObject(arr: string[]) {
+ return arr.reduce((acc, item) => {
+ return { ...acc, [item]: true } as const;
+ }, {} as Record);
+}
+
+function filterDataPaths(paths: string[], entityTree: EntityTree) {
+ const dataPaths: string[] = [];
+ for (const path of paths) {
+ const { entityName } = getEntityNameAndPropertyPath(path);
+ const entity = entityTree.getEntityByName(entityName);
+ if (!entity || !PathUtils.isDataPath(path, entity)) continue;
+ dataPaths.push(path);
+ }
+ return dataPaths;
+}
+function getAllEntityActions(entityTree: EntityTree) {
+ const allEntityActions = new Set();
+ for (const [entityName, entity] of Object.entries(entityTree.getRawTree())) {
+ for (const entityFnDescription of entityFns) {
+ if (entityFnDescription.qualifier(entity)) {
+ const fullPath = `${
+ entityFnDescription.path ||
+ `${entityName}.${entityFnDescription.name}`
+ }`;
+ allEntityActions.add(fullPath);
+ }
+ }
+ }
+ return [...allEntityActions];
+}
+
+export const lintService = new LintService();
diff --git a/app/client/src/plugins/Linting/handlers/updateJSLibraryGlobals.ts b/app/client/src/plugins/Linting/handlers/updateJSLibraryGlobals.ts
new file mode 100644
index 0000000000..a2b552cbdb
--- /dev/null
+++ b/app/client/src/plugins/Linting/handlers/updateJSLibraryGlobals.ts
@@ -0,0 +1,23 @@
+import type { updateJSLibraryProps } from "plugins/Linting/types";
+import { isEqual } from "lodash";
+import { JSLibraries } from "workers/common/JSLibrary";
+import { resetJSLibraries } from "workers/common/JSLibrary/resetJSLibraries";
+
+export function updateJSLibraryGlobals(data: updateJSLibraryProps) {
+ const { add, libs } = data;
+ 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;
+}
diff --git a/app/client/src/plugins/Linting/lib/entity/EntityTree.ts b/app/client/src/plugins/Linting/lib/entity/EntityTree.ts
new file mode 100644
index 0000000000..fa454ba014
--- /dev/null
+++ b/app/client/src/plugins/Linting/lib/entity/EntityTree.ts
@@ -0,0 +1,114 @@
+import type {
+ ConfigTree,
+ DataTree,
+ DataTreeEntity,
+} from "entities/DataTree/dataTreeFactory";
+import type { IEntity } from ".";
+import type { Diff } from "deep-diff";
+import EntityFactory from ".";
+import { PathUtils } from "plugins/Linting/utils/pathUtils";
+import { isJSAction } from "@appsmith/workers/Evaluation/evaluationUtils";
+import type { EntityParser } from "plugins/Linting/utils/entityParser";
+import {
+ DefaultEntityParser,
+ JSLintEntityParser,
+} from "plugins/Linting/utils/entityParser";
+import type { EntityDiffGenerator } from "plugins/Linting/utils/diffGenerator";
+import {
+ DefaultDiffGenerator,
+ JSLintDiffGenerator,
+} from "plugins/Linting/utils/diffGenerator";
+import { union } from "lodash";
+
+export abstract class EntityTree {
+ protected tree = new Map();
+ protected unEvalTree: DataTree = {};
+ protected configTree: ConfigTree = {};
+ constructor(unEvalTree: DataTree, configTree: ConfigTree) {
+ this.unEvalTree = unEvalTree;
+ this.configTree = configTree;
+ }
+ abstract buildTree(unEvalTree: DataTree, configTree: ConfigTree): void;
+ computeDifferences(newTree: EntityTree) {
+ const differences: Diff[] = [];
+ if (!newTree) return differences;
+ const entityNames = Object.keys(this.getRawTree());
+ const newEntityNames = Object.keys(newTree.getRawTree());
+ const allEntityNames = union(entityNames, newEntityNames);
+ for (const entityName of allEntityNames) {
+ const entity = this.getEntityByName(entityName);
+ const newEntity = newTree.getEntityByName(entityName);
+
+ if (!newEntity) {
+ differences.push({
+ path: [entityName],
+ kind: "D",
+ lhs: entity?.getRawEntity(),
+ });
+ continue;
+ }
+ const difference = newEntity.computeDifference(entity);
+ if (!difference) continue;
+ differences.push(...difference);
+ }
+ return differences;
+ }
+
+ getAllPaths = (): Record => {
+ return PathUtils.getAllPaths(this.unEvalTree);
+ };
+
+ getRawTree() {
+ const rawTree: DataTree = {};
+ for (const [name, entity] of this.tree.entries()) {
+ rawTree[name] = entity.getRawEntity() as DataTreeEntity;
+ }
+ return rawTree as DataTree;
+ }
+
+ getEntityByName(name: string) {
+ return this.tree.get(name);
+ }
+
+ getEntities() {
+ const entities = Array.from(this.tree.values());
+ return entities;
+ }
+}
+
+export interface EntityClassLoader {
+ load(entity: DataTreeEntity): {
+ Parser: { new (): EntityParser };
+ DiffGenerator: { new (): EntityDiffGenerator };
+ };
+}
+
+class LintEntityClassLoader implements EntityClassLoader {
+ load(entity: DataTreeEntity) {
+ if (isJSAction(entity)) {
+ return {
+ Parser: JSLintEntityParser,
+ DiffGenerator: JSLintDiffGenerator,
+ };
+ }
+ return {
+ Parser: DefaultEntityParser,
+ DiffGenerator: DefaultDiffGenerator,
+ };
+ }
+}
+
+export class LintEntityTree extends EntityTree {
+ constructor(unEvalTree: DataTree, configTree: ConfigTree) {
+ super(unEvalTree, configTree);
+ this.buildTree(unEvalTree, configTree);
+ }
+ buildTree(unEvalTree: DataTree, configTree: ConfigTree): void {
+ const entities = Object.entries(unEvalTree);
+ const classLoader = new LintEntityClassLoader();
+ for (const [name, entity] of entities) {
+ const config = configTree[name];
+ this.tree.set(name, EntityFactory.getEntity(entity, config, classLoader));
+ }
+ }
+}
diff --git a/app/client/src/plugins/Linting/lib/entity/index.ts b/app/client/src/plugins/Linting/lib/entity/index.ts
new file mode 100644
index 0000000000..1d00489b67
--- /dev/null
+++ b/app/client/src/plugins/Linting/lib/entity/index.ts
@@ -0,0 +1,309 @@
+import {
+ isAction,
+ isAppsmithEntity as isAppsmith,
+ isJSAction,
+ isWidget,
+} from "@appsmith/workers/Evaluation/evaluationUtils";
+import type {
+ JSActionEntity as TJSActionEntity,
+ ActionEntity as TActionEntity,
+ PagelistEntity as TPageListEntity,
+ ActionEntityConfig as TActionEntityConfig,
+ JSActionEntityConfig as TJSActionEntityConfig,
+} from "entities/DataTree/types";
+import type {
+ WidgetEntity as TWidgetEntity,
+ AppsmithEntity as TAppsmithEntity,
+ DataTreeEntityConfig,
+ DataTreeEntity,
+ WidgetEntityConfig as TWidgetEntityConfig,
+} from "entities/DataTree/dataTreeFactory";
+import {
+ defaultDiffGenerator,
+ type EntityDiffGenerator,
+} from "plugins/Linting/utils/diffGenerator";
+import type { EntityParser } from "plugins/Linting/utils/entityParser";
+import type { Diff } from "deep-diff";
+import type { EntityClassLoader } from "./EntityTree";
+
+import type { TParsedJSProperty } from "@shared/ast";
+import { isJSFunctionProperty } from "@shared/ast";
+
+enum ENTITY_TYPE {
+ ACTION = "ACTION",
+ WIDGET = "WIDGET",
+ APPSMITH = "APPSMITH",
+ JSACTION = "JSACTION",
+ PAGELIST = "PAGELIST",
+}
+
+export interface IEntity {
+ getName(): string;
+ getId(): string;
+ getType(): ENTITY_TYPE;
+ getRawEntity(): unknown;
+ getConfig(): unknown;
+ computeDifference(entity?: IEntity): Diff[] | undefined;
+}
+
+export default class EntityFactory {
+ static getEntity<
+ T extends DataTreeEntity,
+ K extends DataTreeEntityConfig | undefined,
+ >(entity: T, config: K, classLoader: EntityClassLoader): IEntity {
+ const { DiffGenerator, Parser } = classLoader.load(entity);
+ if (isWidget(entity)) {
+ return new WidgetEntity(
+ entity,
+ config as TWidgetEntityConfig,
+ new Parser(),
+ new DiffGenerator(),
+ );
+ } else if (isJSAction(entity)) {
+ return new JSEntity(
+ entity,
+ config as TJSActionEntityConfig,
+ new Parser(),
+ new DiffGenerator(),
+ );
+ } else if (isAction(entity)) {
+ return new ActionEntity(
+ entity,
+ config as TActionEntityConfig,
+ new Parser(),
+ new DiffGenerator(),
+ );
+ } else if (isAppsmith(entity)) {
+ return new AppsmithEntity(
+ entity,
+ undefined,
+ new Parser(),
+ new DiffGenerator(),
+ );
+ } else {
+ return new PagelistEntity(entity as TPageListEntity, undefined);
+ }
+ }
+}
+
+export class ActionEntity implements IEntity {
+ private entity: TActionEntity;
+ private config: TActionEntityConfig;
+ entityParser: EntityParser;
+ diffGenerator: EntityDiffGenerator = defaultDiffGenerator;
+ constructor(
+ entity: TActionEntity,
+ config: TActionEntityConfig,
+ entityParser: EntityParser,
+ diffGenerator: EntityDiffGenerator,
+ ) {
+ this.entity = entity;
+ this.config = config;
+ this.entityParser = entityParser;
+ this.diffGenerator = diffGenerator;
+ }
+ getType() {
+ return ENTITY_TYPE.ACTION;
+ }
+ getRawEntity() {
+ return this.entityParser.parse(this.entity, this.config).parsedEntity;
+ }
+ getName() {
+ return this.config.name;
+ }
+ getId() {
+ return this.config.actionId;
+ }
+ getConfig() {
+ return this.config;
+ }
+ computeDifference(oldEntity?: IEntity): Diff[] | undefined {
+ return this.diffGenerator.generate(oldEntity, this);
+ }
+}
+
+export class WidgetEntity implements IEntity {
+ private entity: TWidgetEntity;
+ private config: TWidgetEntityConfig;
+ entityParser: EntityParser;
+ diffGenerator: EntityDiffGenerator = defaultDiffGenerator;
+ constructor(
+ entity: TWidgetEntity,
+ config: TWidgetEntityConfig,
+ entityParser: EntityParser,
+ diffGenerator: EntityDiffGenerator,
+ ) {
+ this.entity = entity;
+ this.config = config;
+ this.entityParser = entityParser;
+ this.diffGenerator = diffGenerator;
+ }
+ getType(): ENTITY_TYPE {
+ return ENTITY_TYPE.WIDGET;
+ }
+ getRawEntity() {
+ return this.entityParser.parse(this.entity, this.config).parsedEntity;
+ }
+ getName() {
+ return this.entity.widgetName;
+ }
+ getId() {
+ return this.config.widgetId as string;
+ }
+ getConfig() {
+ return this.config;
+ }
+ computeDifference(oldEntity?: IEntity): Diff[] | undefined {
+ return this.diffGenerator.generate(oldEntity, this);
+ }
+}
+
+export class JSEntity implements IEntity {
+ entity: TJSActionEntity;
+ private config: TJSActionEntityConfig;
+ entityParser: EntityParser;
+ diffGenerator: EntityDiffGenerator = defaultDiffGenerator;
+
+ constructor(
+ entity: TJSActionEntity,
+ config: TJSActionEntityConfig,
+ entityParser: EntityParser,
+ diffGenerator: EntityDiffGenerator,
+ ) {
+ entityParser.parse(entity, config);
+ this.entity = entity;
+ this.config = config;
+ this.entityParser = entityParser;
+ this.diffGenerator = diffGenerator;
+ }
+ getType() {
+ return ENTITY_TYPE.JSACTION;
+ }
+ getRawEntity() {
+ return this.entity;
+ }
+ getConfig() {
+ return this.config;
+ }
+ getName() {
+ return this.config.name;
+ }
+ getId() {
+ return this.config.actionId;
+ }
+ isEqual(body: string) {
+ return body === this.getRawEntity().body;
+ }
+ computeDifference(oldEntity?: IEntity): Diff[] | undefined {
+ return this.diffGenerator.generate(oldEntity, this);
+ }
+ getFns() {
+ const jsFunctions = [];
+ const { parsedEntity, parsedEntityConfig } = this.entityParser.parse(
+ this.entity,
+ this.config,
+ );
+ for (const propertyName of Object.keys(parsedEntityConfig)) {
+ const jsPropertyConfig = parsedEntityConfig[
+ propertyName
+ ] as TParsedJSProperty;
+ const jsPropertyFullName = `${this.getName()}.${propertyName}`;
+ if (!isJSFunctionProperty(jsPropertyConfig)) continue;
+ jsFunctions.push({
+ name: jsPropertyFullName,
+ body: parsedEntity[propertyName],
+ isMarkedAsync: jsPropertyConfig.isMarkedAsync,
+ });
+ }
+ return jsFunctions;
+ }
+}
+export class PagelistEntity implements IEntity {
+ private entity: TPageListEntity;
+ private config: undefined;
+ constructor(entity: TPageListEntity, config: undefined) {
+ this.entity = entity;
+ this.config = config;
+ }
+ getType() {
+ return ENTITY_TYPE.PAGELIST;
+ }
+ getConfig() {
+ return this.config;
+ }
+ getRawEntity() {
+ return this.entity;
+ }
+ getName() {
+ return "pageList";
+ }
+ getId() {
+ return "pageList";
+ }
+ computeDifference(): Diff[] | undefined {
+ return;
+ }
+}
+
+export class AppsmithEntity implements IEntity {
+ private entity: TAppsmithEntity;
+ private config: undefined;
+ entityParser: EntityParser;
+ diffGenerator: EntityDiffGenerator;
+ constructor(
+ entity: TAppsmithEntity,
+ config: undefined,
+ entityParser: EntityParser,
+ diffGenerator: EntityDiffGenerator,
+ ) {
+ this.entity = entity;
+ this.config = config;
+ this.entityParser = entityParser;
+ this.diffGenerator = diffGenerator;
+ }
+ getType() {
+ return ENTITY_TYPE.APPSMITH;
+ }
+ getConfig() {
+ return this.config;
+ }
+ getRawEntity(): TAppsmithEntity {
+ return this.entity;
+ }
+ getName() {
+ return "appsmith";
+ }
+ getId(): string {
+ return "appsmith";
+ }
+ computeDifference(oldEntity?: IEntity): Diff[] | undefined {
+ return this.diffGenerator.generate(oldEntity, this);
+ }
+}
+
+export function isJSEntity(entity: IEntity): entity is JSEntity {
+ return entity.getType() === ENTITY_TYPE.JSACTION;
+}
+export function isActionEntity(entity: IEntity): entity is ActionEntity {
+ return entity.getType() === ENTITY_TYPE.ACTION;
+}
+export function isAppsmithEntity(entity: IEntity): entity is AppsmithEntity {
+ return entity.getType() === ENTITY_TYPE.APPSMITH;
+}
+export function isWidgetEntity(entity: IEntity): entity is WidgetEntity {
+ return entity.getType() === ENTITY_TYPE.WIDGET;
+}
+export function isPagelistEntity(entity: IEntity): entity is PagelistEntity {
+ return entity.getType() === ENTITY_TYPE.PAGELIST;
+}
+
+// only Widgets, jsActions and Actions have paths that can be dynamic
+export function isDynamicEntity(
+ entity: IEntity,
+): entity is JSEntity | WidgetEntity | ActionEntity {
+ return [
+ ENTITY_TYPE.JSACTION,
+ ENTITY_TYPE.WIDGET,
+ ENTITY_TYPE.ACTION,
+ ].includes(entity.getType());
+}
diff --git a/app/client/src/workers/Linting/index.ts b/app/client/src/plugins/Linting/lintTree.ts
similarity index 90%
rename from app/client/src/workers/Linting/index.ts
rename to app/client/src/plugins/Linting/lintTree.ts
index 04af18c60d..3240ba8a24 100644
--- a/app/client/src/workers/Linting/index.ts
+++ b/app/client/src/plugins/Linting/lintTree.ts
@@ -3,26 +3,26 @@ import { get, isEmpty, set } from "lodash";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { LintError } from "utils/DynamicBindingUtils";
import { globalData } from "./globalData";
-import type {
- getlintErrorsFromTreeProps,
- getlintErrorsFromTreeResponse,
-} from "./types";
import lintBindingPath from "./utils/lintBindingPath";
import lintTriggerPath from "./utils/lintTriggerPath";
import lintJSObjectBody from "./utils/lintJSObjectBody";
import sortLintingPathsByType from "./utils/sortLintingPathsByType";
import lintJSObjectProperty from "./utils/lintJSObjectProperty";
+import type {
+ getLintErrorsFromTreeProps,
+ getLintErrorsFromTreeResponse,
+} from "./types";
-export function getlintErrorsFromTree({
+export function getLintErrorsFromTree({
asyncJSFunctionsInDataFields,
cloudHosting,
configTree,
jsPropertiesState,
pathsToLint,
unEvalTree,
-}: getlintErrorsFromTreeProps): getlintErrorsFromTreeResponse {
+}: getLintErrorsFromTreeProps): getLintErrorsFromTreeResponse {
const lintTreeErrors: LintErrorsStore = {};
- const updatedJSEntities = new Set();
+ const lintedJSPaths = new Set();
globalData.initialize(unEvalTree, cloudHosting);
const { bindingPaths, jsObjectPaths, triggerPaths } = sortLintingPathsByType(
pathsToLint,
@@ -72,16 +72,17 @@ export function getlintErrorsFromTree({
getEntityNameAndPropertyPath(jsObjectPath);
const jsObjectState = get(jsPropertiesState, jsObjectName);
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
- updatedJSEntities.add(jsObjectName);
// An empty state shows that there is a parse error in the jsObject or the object is empty, so we lint the entire body
// instead of an individual properties
if (isEmpty(jsObjectState)) {
+ lintedJSPaths.add(`${jsObjectName}.body`);
const jsObjectBodyLintErrors = lintJSObjectBody(
jsObjectName,
globalData.getGlobalData(true),
);
set(lintTreeErrors, jsObjectBodyPath, jsObjectBodyLintErrors);
} else if (jsPropertyName !== "body") {
+ lintedJSPaths.add(jsObjectPath);
const propertyLintErrors = lintJSObjectProperty(
jsObjectPath,
jsObjectState,
@@ -103,6 +104,6 @@ export function getlintErrorsFromTree({
return {
errors: lintTreeErrors,
- updatedJSEntities: Array.from(updatedJSEntities),
+ lintedJSPaths: Array.from(lintedJSPaths),
};
}
diff --git a/app/client/src/plugins/Linting/linters/index.ts b/app/client/src/plugins/Linting/linters/index.ts
new file mode 100644
index 0000000000..a16b47d21c
--- /dev/null
+++ b/app/client/src/plugins/Linting/linters/index.ts
@@ -0,0 +1,59 @@
+import { GracefulWorkerService } from "utils/WorkerUtil";
+import type {
+ LintTreeRequestPayload,
+ updateJSLibraryProps,
+} from "plugins/Linting/types";
+import { LINT_WORKER_ACTIONS as LINT_ACTIONS } from "plugins/Linting/types";
+import { handlerMap } from "plugins/Linting/handlers";
+
+export interface ILinter {
+ lintTree(args: LintTreeRequestPayload): any;
+ updateJSLibraryGlobals(args: updateJSLibraryProps): any;
+ start(): void;
+ shutdown(): void;
+}
+
+export class BaseLinter implements ILinter {
+ lintTree(args: LintTreeRequestPayload) {
+ return handlerMap[LINT_ACTIONS.LINT_TREE](args);
+ }
+ updateJSLibraryGlobals(args: updateJSLibraryProps) {
+ return handlerMap[LINT_ACTIONS.UPDATE_LINT_GLOBALS](args);
+ }
+ start() {
+ return;
+ }
+ shutdown() {
+ return;
+ }
+}
+
+export class WorkerLinter implements ILinter {
+ server: GracefulWorkerService;
+ constructor() {
+ this.server = new GracefulWorkerService(
+ new Worker(new URL("./worker.ts", import.meta.url), {
+ type: "module",
+ // Note: the `Worker` part of the name is slightly important – LinkRelPreload_spec.js
+ // relies on it to find workers in the list of all requests.
+ name: "lintWorker",
+ }),
+ );
+ this.start = this.start.bind(this);
+ this.shutdown = this.shutdown.bind(this);
+ this.lintTree = this.lintTree.bind(this);
+ this.updateJSLibraryGlobals = this.updateJSLibraryGlobals.bind(this);
+ }
+ *start() {
+ yield* this.server.start();
+ }
+ *shutdown() {
+ yield* this.server.shutdown();
+ }
+ *lintTree(args: LintTreeRequestPayload) {
+ return yield* this.server.request(LINT_ACTIONS.LINT_TREE, args);
+ }
+ *updateJSLibraryGlobals(args: updateJSLibraryProps) {
+ return yield* this.server.request(LINT_ACTIONS.UPDATE_LINT_GLOBALS, args);
+ }
+}
diff --git a/app/client/src/plugins/Linting/linters/worker.ts b/app/client/src/plugins/Linting/linters/worker.ts
new file mode 100644
index 0000000000..99b9b56453
--- /dev/null
+++ b/app/client/src/plugins/Linting/linters/worker.ts
@@ -0,0 +1,26 @@
+import type { TMessage } from "utils/MessageUtil";
+import { MessageType } from "utils/MessageUtil";
+import { WorkerMessenger } from "workers/Evaluation/fns/utils/Messenger";
+import DependencyMap from "entities/DependencyMap";
+import type { LintRequest } from "../types";
+import { handlerMap } from "../handlers";
+
+export const triggerFieldDependency = new DependencyMap();
+export const actionInDataFieldDependency = new DependencyMap();
+
+export function messageListener(e: MessageEvent>) {
+ const { messageType } = e.data;
+ if (messageType !== MessageType.REQUEST) return;
+ const startTime = performance.now();
+ const { body, messageId } = e.data;
+ const { data, method } = body;
+ if (!method) return;
+ const messageHandler = handlerMap[method];
+ if (typeof messageHandler !== "function") return;
+ const responseData = messageHandler(data);
+ if (!responseData) return;
+ const endTime = performance.now();
+ WorkerMessenger.respond(messageId, responseData, endTime - startTime);
+}
+
+self.onmessage = messageListener;
diff --git a/app/client/src/workers/Linting/types.ts b/app/client/src/plugins/Linting/types.ts
similarity index 74%
rename from app/client/src/workers/Linting/types.ts
rename to app/client/src/plugins/Linting/types.ts
index 051ed403c4..d8d835c746 100644
--- a/app/client/src/workers/Linting/types.ts
+++ b/app/client/src/plugins/Linting/types.ts
@@ -4,59 +4,52 @@ import type {
DataTreeEntity,
} from "entities/DataTree/dataTreeFactory";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
-import type { WorkerRequest } from "@appsmith/workers/common/types";
import type {
createEvaluationContext,
EvaluationScriptType,
} from "workers/Evaluation/evaluate";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
+import type { TJSLibrary } from "workers/common/JSLibrary";
export enum LINT_WORKER_ACTIONS {
LINT_TREE = "LINT_TREE",
UPDATE_LINT_GLOBALS = "UPDATE_LINT_GLOBALS",
}
-
export interface LintTreeResponse {
errors: LintErrorsStore;
- updatedJSEntities: string[];
+ lintedJSPaths: string[];
+ jsPropertiesState: TJSPropertiesState;
}
-export interface LintTreeRequest {
- pathsToLint: string[];
+export interface LintTreeRequestPayload {
unevalTree: DataTree;
- jsPropertiesState: TJSPropertiesState;
configTree: ConfigTree;
cloudHosting: boolean;
- asyncJSFunctionsInDataFields: DependencyMap;
+ forceLinting?: boolean;
}
-export type LintWorkerRequest = WorkerRequest<
- LintTreeRequest,
- LINT_WORKER_ACTIONS
->;
-
-export type LintTreeSagaRequestData = {
- pathsToLint: string[];
- unevalTree: DataTree;
- jsPropertiesState: TJSPropertiesState;
- asyncJSFunctionsInDataFields: DependencyMap;
- configTree: ConfigTree;
+export type LintRequest = {
+ data: any;
+ method: LINT_WORKER_ACTIONS;
};
+export type LintTreeSagaRequestData = {
+ unevalTree: DataTree;
+ configTree: ConfigTree;
+ forceLinting?: boolean;
+};
export interface lintTriggerPathProps {
userScript: string;
entity: DataTreeEntity;
globalData: ReturnType;
}
-
export interface lintBindingPathProps {
dynamicBinding: string;
entity: DataTreeEntity;
fullPropertyPath: string;
globalData: ReturnType;
}
-
export interface getLintingErrorsProps {
script: string;
data: Record;
@@ -68,7 +61,7 @@ export interface getLintingErrorsProps {
};
}
-export interface getlintErrorsFromTreeProps {
+export interface getLintErrorsFromTreeProps {
pathsToLint: string[];
unEvalTree: DataTree;
jsPropertiesState: TJSPropertiesState;
@@ -77,7 +70,12 @@ export interface getlintErrorsFromTreeProps {
configTree: ConfigTree;
}
-export interface getlintErrorsFromTreeResponse {
+export interface getLintErrorsFromTreeResponse {
errors: LintErrorsStore;
- updatedJSEntities: string[];
+ lintedJSPaths: string[];
+}
+
+export interface updateJSLibraryProps {
+ add?: boolean;
+ libs: TJSLibrary[];
}
diff --git a/app/client/src/plugins/Linting/utils/diffGenerator.ts b/app/client/src/plugins/Linting/utils/diffGenerator.ts
new file mode 100644
index 0000000000..09b2db850e
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/diffGenerator.ts
@@ -0,0 +1,68 @@
+import type { TParsedJSProperty } from "@shared/ast";
+import type { JSEntity, IEntity } from "plugins/Linting/lib/entity";
+import type { Diff } from "deep-diff";
+import { diff } from "deep-diff";
+import type { jsLintEntityParser } from "./entityParser";
+
+export interface EntityDiffGenerator {
+ generate(
+ baseEntity?: IEntity,
+ compareEntity?: IEntity,
+ ): Diff[] | undefined;
+}
+
+export class DefaultDiffGenerator implements EntityDiffGenerator {
+ generate(baseEntity?: IEntity, compareEntity?: IEntity) {
+ return diff(
+ this.generateDiffObj(baseEntity),
+ this.generateDiffObj(compareEntity),
+ );
+ }
+ generateDiffObj(entity?: IEntity) {
+ if (!entity) {
+ return {};
+ }
+ return { [entity.getName()]: entity.getRawEntity() };
+ }
+}
+
+export class JSLintDiffGenerator implements EntityDiffGenerator {
+ generate(baseEntity?: JSEntity, compareEntity?: JSEntity) {
+ return diff(
+ this.generateDiffObj(baseEntity),
+ this.generateDiffObj(compareEntity),
+ );
+ }
+ generateDiffObj(entity?: JSEntity) {
+ if (!entity) {
+ return {};
+ }
+
+ const entityForDiff: Record = {};
+ for (const [propertyName, propertyValue] of Object.entries(
+ entity.getRawEntity(),
+ )) {
+ const jsParser = entity.entityParser as typeof jsLintEntityParser;
+ const { parsedEntityConfig } = jsParser.parse(
+ entity.getRawEntity(),
+ entity.getConfig(),
+ );
+ if (!parsedEntityConfig) continue;
+ entityForDiff[propertyName] = this.getHashedConfigString(
+ propertyValue,
+ parsedEntityConfig[propertyName] as TParsedJSProperty,
+ );
+ }
+ return { [entity.getName()]: entityForDiff };
+ }
+
+ getHashedConfigString(propertyValue: string, config: TParsedJSProperty) {
+ if (!config || !config.position || !config.value) return propertyValue;
+ const { endColumn, endLine, startColumn, startLine } = config.position;
+
+ return config.value + `${startColumn}${endColumn}${startLine}${endLine}`;
+ }
+}
+
+export const jsLintDiffGenerator = new JSLintDiffGenerator();
+export const defaultDiffGenerator = new DefaultDiffGenerator();
diff --git a/app/client/src/plugins/Linting/utils/entityParser.ts b/app/client/src/plugins/Linting/utils/entityParser.ts
new file mode 100644
index 0000000000..217b5fe52d
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/entityParser.ts
@@ -0,0 +1,170 @@
+import type {
+ DataTreeEntity,
+ DataTreeEntityConfig,
+} from "entities/DataTree/dataTreeFactory";
+import type {
+ JSActionEntityConfig,
+ JSActionEntity as TJSActionEntity,
+} from "entities/DataTree/types";
+import { EvaluationSubstitutionType } from "entities/DataTree/types";
+import type { TParsedJSProperty } from "@shared/ast";
+import { isJSFunctionProperty } from "@shared/ast";
+import { parseJSObject } from "@shared/ast";
+import type { JSVarProperty } from "@shared/ast";
+import type { JSFunctionProperty } from "@shared/ast";
+import { uniq } from "lodash";
+import { validJSBodyRegex } from "workers/Evaluation/JSObject";
+
+export interface EntityParser {
+ parse(
+ entity: T,
+ entityConfig: K,
+ ): ParsedEntity;
+ parse(
+ entity: T,
+ entityConfig: K,
+ ): ParsedEntity;
+}
+
+type TParsedJSEntity = Record & {
+ body: string;
+};
+
+type TParsedJSEntityConfig = Record;
+
+export type ParsedJSCache = {
+ parsedEntity: ParsedEntity;
+ parsedEntityConfig: TParsedJSEntityConfig;
+};
+
+export type ParsedEntity = {
+ parsedEntity: Partial;
+ parsedEntityConfig: Record;
+};
+
+export class DefaultEntityParser implements EntityParser {
+ parse(entity: T) {
+ return {
+ parsedEntity: entity,
+ parsedEntityConfig: {},
+ };
+ }
+}
+
+export class JSLintEntityParser implements EntityParser {
+ #parsedJSCache: ParsedEntity = {
+ parsedEntity: {},
+ parsedEntityConfig: {},
+ };
+ parse(entity: TJSActionEntity, entityConfig: JSActionEntityConfig) {
+ const jsEntityBody = entity.body;
+ if (
+ this.#parsedJSCache &&
+ jsEntityBody === this.#parsedJSCache.parsedEntity.body
+ ) {
+ return {
+ parsedEntity: this.#parsedJSCache.parsedEntity,
+ parsedEntityConfig: this.#parsedJSCache.parsedEntityConfig,
+ };
+ }
+
+ const { parsedObject, success } = this.#parseJSObjectBody(jsEntityBody);
+
+ const parsedJSEntityConfig: Record = {};
+ const parsedJSEntity: TParsedJSEntity = { body: jsEntityBody };
+
+ if (success) {
+ for (const [propertyName, parsedPropertyDetails] of Object.entries(
+ parsedObject,
+ )) {
+ const { position, rawContent, type, value } = parsedPropertyDetails;
+ parsedJSEntity[propertyName] = value;
+ if (isJSFunctionProperty(parsedPropertyDetails)) {
+ parsedJSEntityConfig[propertyName] = {
+ isMarkedAsync: parsedPropertyDetails.isMarkedAsync,
+ position,
+ value: rawContent,
+ type,
+ } as JSFunctionProperty;
+ } else if (type !== "literal") {
+ parsedJSEntityConfig[propertyName] = {
+ position: position,
+ value: rawContent,
+ type,
+ } as JSVarProperty;
+ }
+ }
+ }
+ // Save parsed entity to cache
+ this.#parsedJSCache = {
+ parsedEntity: parsedJSEntity,
+ parsedEntityConfig: parsedJSEntityConfig,
+ };
+
+ // update entity and entity config
+ const requiredProps = ["actionId", "body", "ENTITY_TYPE"];
+ for (const property of Object.keys(entity)) {
+ if (requiredProps.includes(property)) continue;
+ delete entity[property];
+ delete entityConfig.reactivePaths[property];
+ }
+
+ for (const [propertyName, propertyValue] of Object.entries(
+ parsedJSEntity,
+ )) {
+ entity[propertyName] = propertyValue;
+ entityConfig.reactivePaths[propertyName] =
+ EvaluationSubstitutionType.TEMPLATE;
+ const propertyConfig = parsedJSEntityConfig[
+ propertyName
+ ] as TParsedJSProperty;
+ if (propertyConfig && isJSFunctionProperty(propertyConfig)) {
+ entity[`${propertyName}.data`] = {};
+ }
+ }
+ return this.#parsedJSCache;
+ }
+
+ #isValidJSBody(jsBody: string) {
+ return !!jsBody.trim() && validJSBodyRegex.test(jsBody);
+ }
+
+ #parseJSObjectBody = (jsBody: string) => {
+ const unsuccessfulParsingResponse = {
+ success: false,
+ parsedObject: {},
+ } as const;
+ let response:
+ | { success: false; parsedObject: Record }
+ | { success: true; parsedObject: Record } =
+ unsuccessfulParsingResponse;
+
+ if (this.#isValidJSBody(jsBody)) {
+ const { parsedObject: parsedProperties, success } = parseJSObject(jsBody);
+ if (success) {
+ // When a parsed object has duplicate keys, the jsobject is invalid and its body (not individual properties) needs to be linted
+ // so we return an empty object
+ const allPropertyKeys = parsedProperties.map(
+ (property) => property.key,
+ );
+ const uniqueKeys = uniq(allPropertyKeys);
+ const hasUniqueKeys = allPropertyKeys.length === uniqueKeys.length;
+ if (hasUniqueKeys) {
+ response = {
+ success: true,
+ parsedObject: parsedProperties.reduce(
+ (acc: Record, property) => {
+ const updatedProperties = { ...acc, [property.key]: property };
+ return updatedProperties;
+ },
+ {},
+ ),
+ } as const;
+ }
+ }
+ }
+ return response;
+ };
+}
+
+export const jsLintEntityParser = new JSLintEntityParser();
diff --git a/app/client/src/plugins/Linting/utils/getEntityDependencies.ts b/app/client/src/plugins/Linting/utils/getEntityDependencies.ts
new file mode 100644
index 0000000000..4208352c72
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/getEntityDependencies.ts
@@ -0,0 +1,289 @@
+import {
+ addWidgetPropertyDependencies,
+ convertPathToString,
+ getEntityNameAndPropertyPath,
+} from "@appsmith/workers/Evaluation/evaluationUtils";
+import { ENTITY_TYPE } from "entities/DataTree/types";
+import type { DependencyMap as TDependencyMap } from "utils/DynamicBindingUtils";
+import { getPropertyPath } from "utils/DynamicBindingUtils";
+import { getDynamicBindings } from "utils/DynamicBindingUtils";
+import { getEntityDynamicBindingPathList } from "utils/DynamicBindingUtils";
+import { mergeMaps } from "./mergeMaps";
+import { flatten, get, has, isString, toPath, union, uniq } from "lodash";
+import { extractIdentifierInfoFromCode } from "@shared/ast";
+import { PathUtils } from "./pathUtils";
+import type {
+ ActionEntity,
+ IEntity,
+ JSEntity,
+ WidgetEntity,
+} from "../lib/entity";
+import type { DataTreeEntity } from "entities/DataTree/dataTreeFactory";
+
+export function getEntityDependencies(
+ entity: IEntity,
+): TDependencyMap | undefined {
+ switch (entity.getType()) {
+ case ENTITY_TYPE.ACTION:
+ return getActionDependencies(entity as ActionEntity);
+ case ENTITY_TYPE.JSACTION:
+ return getJSDependencies(entity as JSEntity);
+ case ENTITY_TYPE.WIDGET:
+ return getWidgetDependencies(entity as WidgetEntity);
+ default:
+ return undefined;
+ }
+}
+function getWidgetDependencies(widgetEntity: WidgetEntity): TDependencyMap {
+ let dependencies: TDependencyMap = {};
+ const widgetConfig = widgetEntity.getConfig();
+ const widgetName = widgetEntity.getName();
+
+ const widgetInternalDependencies = addWidgetPropertyDependencies({
+ widgetConfig,
+ widgetName,
+ });
+
+ dependencies = mergeMaps(dependencies, widgetInternalDependencies);
+
+ const dynamicBindingPathList = getEntityDynamicBindingPathList(widgetConfig);
+ const dynamicTriggerPathList = widgetConfig.dynamicTriggerPathList || [];
+ const allDynamicPaths = union(dynamicTriggerPathList, dynamicBindingPathList);
+
+ for (const dynamicPath of allDynamicPaths) {
+ const propertyPath = dynamicPath.key;
+ const dynamicPathDependency = getDependencyFromEntityPath(
+ propertyPath,
+ widgetEntity,
+ );
+ dependencies = mergeMaps(dependencies, dynamicPathDependency);
+ }
+
+ return dependencies;
+}
+function getJSDependencies(jsEntity: JSEntity): TDependencyMap {
+ let dependencies: TDependencyMap = {};
+ const jsActionConfig = jsEntity.getConfig();
+ const jsActionReactivePaths = jsActionConfig.reactivePaths || {};
+
+ for (const reactivePath of Object.keys(jsActionReactivePaths)) {
+ const reactivePathDependency = getDependencyFromEntityPath(
+ reactivePath,
+ jsEntity,
+ );
+ dependencies = mergeMaps(dependencies, reactivePathDependency);
+ }
+ const jsEntityInternalDependencyMap =
+ getEntityInternalDependencyMap(jsEntity);
+ dependencies = mergeMaps(dependencies, jsEntityInternalDependencyMap);
+ return dependencies;
+}
+function getActionDependencies(actionEntity: ActionEntity): TDependencyMap {
+ let dependencies: TDependencyMap = {};
+ const actionConfig = actionEntity.getConfig();
+
+ const actionInternalDependencyMap =
+ getEntityInternalDependencyMap(actionEntity);
+ dependencies = mergeMaps(dependencies, actionInternalDependencyMap);
+
+ const dynamicBindingPathList = getEntityDynamicBindingPathList(actionConfig);
+
+ for (const dynamicPath of dynamicBindingPathList) {
+ const propertyPath = dynamicPath.key;
+ const dynamicPathDependency = getDependencyFromEntityPath(
+ propertyPath,
+ actionEntity,
+ );
+ dependencies = mergeMaps(dependencies, dynamicPathDependency);
+ }
+
+ return dependencies;
+}
+
+function getDependencyFromEntityPath(
+ propertyPath: string,
+ entity: IEntity,
+): TDependencyMap {
+ const unevalPropValue = get(
+ entity.getRawEntity(),
+ propertyPath,
+ "",
+ ).toString();
+ const entityName = entity.getName();
+ const { jsSnippets } = getDynamicBindings(unevalPropValue);
+ const validJSSnippets = jsSnippets.filter((jsSnippet) => !!jsSnippet);
+ const dynamicPathDependency: TDependencyMap = {
+ [`${entityName}.${propertyPath}`]: validJSSnippets,
+ };
+ return dynamicPathDependency;
+}
+
+function getEntityInternalDependencyMap(entity: IEntity) {
+ const entityConfig = entity.getConfig();
+ const entityName = entity.getName();
+ const dependencies: TDependencyMap = {};
+ const internalDependencyMap: TDependencyMap = entityConfig
+ ? (entityConfig as Record).dependencyMap
+ : {};
+
+ for (const [path, pathDependencies] of Object.entries(
+ internalDependencyMap,
+ )) {
+ const fullPropertyPath = `${entityName}.${path}`;
+ const fullPathDependencies = pathDependencies.map(
+ (dependentPath) => `${entityName}.${dependentPath}`,
+ );
+ dependencies[fullPropertyPath] = fullPathDependencies;
+ }
+ return dependencies;
+}
+
+export function getEntityPathDependencies(
+ entity: IEntity,
+ fullPropertyPath: string,
+) {
+ switch (entity.getType()) {
+ case ENTITY_TYPE.ACTION:
+ return getActionPropertyPathDependencies(
+ entity as ActionEntity,
+ fullPropertyPath,
+ );
+ case ENTITY_TYPE.JSACTION:
+ return getJSPropertyPathDependencies(
+ entity as JSEntity,
+ fullPropertyPath,
+ );
+ case ENTITY_TYPE.WIDGET:
+ return getWidgetPropertyPathDependencies(
+ entity as WidgetEntity,
+ fullPropertyPath,
+ );
+ default:
+ return undefined;
+ }
+}
+
+function getWidgetPropertyPathDependencies(
+ widgetEntity: WidgetEntity,
+ fullPropertyPath: string,
+): TDependencyMap {
+ const { propertyPath: entityPropertyPath } =
+ getEntityNameAndPropertyPath(fullPropertyPath);
+ const widgetConfig = widgetEntity.getConfig();
+
+ const dynamicBindingPathList = getEntityDynamicBindingPathList(widgetConfig);
+ const dynamicTriggerPathList = widgetConfig.dynamicTriggerPathList || [];
+ const allDynamicPaths = union(dynamicTriggerPathList, dynamicBindingPathList);
+ const isPathADynamicPath =
+ allDynamicPaths.find(
+ (dynamicPath) => dynamicPath.key === entityPropertyPath,
+ ) !== undefined;
+
+ if (!isPathADynamicPath) return {};
+
+ const dynamicPathDependency = getDependencyFromEntityPath(
+ entityPropertyPath,
+ widgetEntity,
+ );
+
+ return dynamicPathDependency;
+}
+function getJSPropertyPathDependencies(
+ jsEntity: JSEntity,
+ fullPropertyPath: string,
+): TDependencyMap {
+ const { propertyPath: entityPropertyPath } =
+ getEntityNameAndPropertyPath(fullPropertyPath);
+ const jsActionConfig = jsEntity.getConfig();
+ const jsActionReactivePaths = jsActionConfig.reactivePaths || {};
+ const isPathAReactivePath =
+ Object.keys(jsActionReactivePaths).find(
+ (path) => path === entityPropertyPath,
+ ) !== undefined;
+ if (!isPathAReactivePath) return {};
+
+ const reactivePathDependency = getDependencyFromEntityPath(
+ entityPropertyPath,
+ jsEntity,
+ );
+ return reactivePathDependency;
+}
+function getActionPropertyPathDependencies(
+ actionEntity: ActionEntity,
+ fullPropertyPath: string,
+): TDependencyMap {
+ const { propertyPath: entityPropertyPath } =
+ getEntityNameAndPropertyPath(fullPropertyPath);
+ const actionConfig = actionEntity.getConfig();
+
+ const dynamicBindingPathList = getEntityDynamicBindingPathList(actionConfig);
+ const isADynamicPath = dynamicBindingPathList.find(
+ (path) => path.key === entityPropertyPath,
+ );
+
+ if (!isADynamicPath) return {};
+
+ const dynamicPathDependency = getDependencyFromEntityPath(
+ entityPropertyPath,
+ actionEntity,
+ );
+
+ return dynamicPathDependency;
+}
+
+export function extractReferencesFromPath(
+ entity: IEntity,
+ fullPropertyPath: string,
+ tree: Record,
+) {
+ if (!PathUtils.isDynamicLeaf(entity, fullPropertyPath)) return [];
+ const entityPropertyPath = getPropertyPath(fullPropertyPath);
+ const rawEntity = entity.getRawEntity() as DataTreeEntity;
+ const propertyPathContent = get(rawEntity, entityPropertyPath);
+ if (!isString(propertyPathContent)) return [];
+
+ const { jsSnippets } = getDynamicBindings(propertyPathContent, rawEntity);
+ const validJSSnippets = jsSnippets.filter((jsSnippet) => !!jsSnippet);
+
+ const referencesInPropertyPath = flatten(
+ validJSSnippets.map((jsSnippet) =>
+ extractReferencesFromJSSnippet(jsSnippet, tree),
+ ),
+ );
+ return referencesInPropertyPath;
+}
+
+export function extractReferencesFromJSSnippet(
+ jsSnippet: string,
+ tree: Record,
+) {
+ const { references } = extractIdentifierInfoFromCode(jsSnippet, 2);
+ const prunedReferences = flatten(
+ references.map((reference) => getPrunedReference(reference, tree)),
+ );
+ return uniq(prunedReferences);
+}
+
+function getPrunedReference(
+ reference: string,
+ tree: Record,
+): string[] {
+ if (has(tree, reference)) {
+ return [reference];
+ }
+ const subpaths = toPath(reference);
+ let currentString = "";
+ const references = [];
+ // We want to keep going till we reach top level
+ while (subpaths.length > 0) {
+ currentString = convertPathToString(subpaths);
+ references.push(currentString);
+ // We've found the dep, add it and return
+ if (has(tree, currentString)) {
+ return references;
+ }
+ subpaths.pop();
+ }
+
+ return references;
+}
diff --git a/app/client/src/workers/Linting/utils/getEvaluationContext.ts b/app/client/src/plugins/Linting/utils/getEvaluationContext.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/getEvaluationContext.ts
rename to app/client/src/plugins/Linting/utils/getEvaluationContext.ts
diff --git a/app/client/src/workers/Linting/utils/getJSToLint.ts b/app/client/src/plugins/Linting/utils/getJSToLint.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/getJSToLint.ts
rename to app/client/src/plugins/Linting/utils/getJSToLint.ts
diff --git a/app/client/src/workers/Linting/utils/getLintSeverity.ts b/app/client/src/plugins/Linting/utils/getLintSeverity.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/getLintSeverity.ts
rename to app/client/src/plugins/Linting/utils/getLintSeverity.ts
diff --git a/app/client/src/workers/Linting/utils/getLintingErrors.ts b/app/client/src/plugins/Linting/utils/getLintingErrors.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/getLintingErrors.ts
rename to app/client/src/plugins/Linting/utils/getLintingErrors.ts
diff --git a/app/client/src/plugins/Linting/utils/groupDifferencesByType.ts b/app/client/src/plugins/Linting/utils/groupDifferencesByType.ts
new file mode 100644
index 0000000000..5ccc730ba5
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/groupDifferencesByType.ts
@@ -0,0 +1,42 @@
+import type { Diff, DiffArray } from "deep-diff";
+import { isEmpty, partition } from "lodash";
+
+export function groupDifferencesByType(differences: Diff[]): {
+ edits: Diff[];
+ additions: Diff[];
+ deletions: Diff[];
+} {
+ if (isEmpty(differences)) return { edits: [], additions: [], deletions: [] };
+ const [edits, others] = partition(differences, (diff) => diff.kind === "E");
+ const [additions, deletionsAndArrayChanges] = partition(
+ others,
+ (diff) => diff.kind === "N",
+ );
+ const [deletions, arrayChanges] = partition(
+ deletionsAndArrayChanges,
+ (diff) => diff.kind === "D",
+ );
+
+ const refinedChanges = (arrayChanges as DiffArray[]).reduce(
+ (acc, currentDiff) => {
+ if (!currentDiff.path) return acc;
+ const { index, item, path } = currentDiff;
+ return [
+ ...acc,
+ {
+ ...item,
+ path: [...path, index],
+ },
+ ];
+ },
+ [] as Diff[],
+ );
+
+ const result = groupDifferencesByType(refinedChanges);
+
+ return {
+ edits: edits.concat(result.edits),
+ additions: additions.concat(result.additions),
+ deletions: deletions.concat(result.deletions),
+ };
+}
diff --git a/app/client/src/workers/Linting/utils/isEntityFunction.ts b/app/client/src/plugins/Linting/utils/isEntityFunction.ts
similarity index 56%
rename from app/client/src/workers/Linting/utils/isEntityFunction.ts
rename to app/client/src/plugins/Linting/utils/isEntityFunction.ts
index 66e61e0cb0..01a3dea124 100644
--- a/app/client/src/workers/Linting/utils/isEntityFunction.ts
+++ b/app/client/src/plugins/Linting/utils/isEntityFunction.ts
@@ -7,9 +7,13 @@ export default function isEntityFunction(
propertyName: string,
) {
if (!isDataTreeEntity(entity)) return false;
- return entityFns.find(
- (entityFn) =>
- entityFn.name === propertyName &&
- entityFn.qualifier(entity as DataTreeEntity),
- );
+ return entityFns.find((entityFn) => {
+ const entityFnpropertyName = entityFn.path
+ ? entityFn.path.split(".")[1]
+ : entityFn.name;
+ return (
+ entityFnpropertyName === propertyName &&
+ entityFn.qualifier(entity as DataTreeEntity)
+ );
+ });
}
diff --git a/app/client/src/workers/Linting/utils/lintBindingPath.ts b/app/client/src/plugins/Linting/utils/lintBindingPath.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/lintBindingPath.ts
rename to app/client/src/plugins/Linting/utils/lintBindingPath.ts
diff --git a/app/client/src/workers/Linting/utils/lintJSObjectBody.ts b/app/client/src/plugins/Linting/utils/lintJSObjectBody.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/lintJSObjectBody.ts
rename to app/client/src/plugins/Linting/utils/lintJSObjectBody.ts
diff --git a/app/client/src/workers/Linting/utils/lintJSObjectProperty.ts b/app/client/src/plugins/Linting/utils/lintJSObjectProperty.ts
similarity index 95%
rename from app/client/src/workers/Linting/utils/lintJSObjectProperty.ts
rename to app/client/src/plugins/Linting/utils/lintJSObjectProperty.ts
index 8aaa69b9de..b526c15783 100644
--- a/app/client/src/workers/Linting/utils/lintJSObjectProperty.ts
+++ b/app/client/src/plugins/Linting/utils/lintJSObjectProperty.ts
@@ -10,6 +10,7 @@ import type {
import { globalData } from "../globalData";
import getLintSeverity from "./getLintSeverity";
import lintJSProperty from "./lintJSProperty";
+import { isEmpty } from "lodash";
export default function lintJSObjectProperty(
jsPropertyFullName: string,
@@ -21,7 +22,8 @@ export default function lintJSObjectProperty(
getEntityNameAndPropertyPath(jsPropertyFullName);
const jsPropertyState = jsObjectState[jsPropertyName];
const isAsyncJSFunctionBoundToSyncField =
- asyncJSFunctionsInDataFields.hasOwnProperty(jsPropertyFullName);
+ asyncJSFunctionsInDataFields.hasOwnProperty(jsPropertyFullName) &&
+ !isEmpty(asyncJSFunctionsInDataFields[jsPropertyFullName]);
const jsPropertyLintErrors = lintJSProperty(
jsPropertyFullName,
@@ -75,6 +77,6 @@ function generateAsyncFunctionBoundToDataFieldCustomError(
code: CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD,
line: jsPropertyState.position.keyStartLine - 1,
ch: jsPropertyState.position.keyStartColumn + 1,
- originalPath: jsPropertyName,
+ originalPath: jsPropertyFullName,
};
}
diff --git a/app/client/src/workers/Linting/utils/lintJSProperty.ts b/app/client/src/plugins/Linting/utils/lintJSProperty.ts
similarity index 85%
rename from app/client/src/workers/Linting/utils/lintJSProperty.ts
rename to app/client/src/plugins/Linting/utils/lintJSProperty.ts
index 2d5ff7c9e1..388db754f2 100644
--- a/app/client/src/workers/Linting/utils/lintJSProperty.ts
+++ b/app/client/src/plugins/Linting/utils/lintJSProperty.ts
@@ -6,7 +6,6 @@ import {
getScriptToEval,
getScriptType,
} from "workers/Evaluation/evaluate";
-import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { TJSpropertyState } from "workers/Evaluation/JSObject/jsPropertiesState";
import getLintingErrors from "./getLintingErrors";
@@ -18,8 +17,7 @@ export default function lintJSProperty(
if (isNil(jsPropertyState)) {
return [];
}
- const { propertyPath: jsPropertyPath } =
- getEntityNameAndPropertyPath(jsPropertyFullName);
+
const scriptType = getScriptType(false, false);
const scriptToLint = getScriptToEval(
jsPropertyState.value,
@@ -40,7 +38,7 @@ export default function lintJSProperty(
lintError.line === 0
? lintError.ch + jsPropertyState.position.startColumn
: lintError.ch,
- originalPath: jsPropertyPath,
+ originalPath: jsPropertyFullName,
};
});
diff --git a/app/client/src/workers/Linting/utils/lintTriggerPath.ts b/app/client/src/plugins/Linting/utils/lintTriggerPath.ts
similarity index 100%
rename from app/client/src/workers/Linting/utils/lintTriggerPath.ts
rename to app/client/src/plugins/Linting/utils/lintTriggerPath.ts
diff --git a/app/client/src/plugins/Linting/utils/mergeMaps.ts b/app/client/src/plugins/Linting/utils/mergeMaps.ts
new file mode 100644
index 0000000000..28bbc18bea
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/mergeMaps.ts
@@ -0,0 +1,12 @@
+import { mergeWith, union } from "lodash";
+import type { DependencyMap } from "utils/DynamicBindingUtils";
+
+export function mergeMaps(firstMap: DependencyMap, secondMap: DependencyMap) {
+ return mergeWith(
+ firstMap,
+ secondMap,
+ (firstVal: string[], secondVal: string[]) => {
+ return union(firstVal, secondVal);
+ },
+ );
+}
diff --git a/app/client/src/plugins/Linting/utils/pathUtils.ts b/app/client/src/plugins/Linting/utils/pathUtils.ts
new file mode 100644
index 0000000000..b7a0852737
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/pathUtils.ts
@@ -0,0 +1,104 @@
+import type { IEntity } from "plugins/Linting/lib/entity";
+import { isDynamicEntity, isWidgetEntity } from "plugins/Linting/lib/entity";
+import {
+ convertPathToString,
+ getEntityNameAndPropertyPath,
+ isTrueObject,
+} from "@appsmith/workers/Evaluation/evaluationUtils";
+import { toPath, union } from "lodash";
+
+export class PathUtils {
+ static getReactivePaths(entity: IEntity) {
+ if (!isDynamicEntity(entity)) return [];
+ const config = entity.getConfig();
+ const name = entity.getName();
+ const reactivePaths = config.reactivePaths;
+ if (!reactivePaths) return [];
+
+ return PathUtils.getFullNamesFromPropertyPaths(
+ Object.keys(reactivePaths),
+ name,
+ );
+ }
+ static getBindingPaths(entity: IEntity) {
+ if (!isDynamicEntity(entity)) return [];
+ const config = entity.getConfig();
+ const name = entity.getName();
+ const bindingPaths = config.bindingPaths;
+ if (!bindingPaths) return [];
+ return PathUtils.getFullNamesFromPropertyPaths(
+ Object.keys(bindingPaths),
+ name,
+ );
+ }
+
+ static getTriggerPaths(entity: IEntity) {
+ if (!isWidgetEntity(entity)) return [];
+ const config = entity.getConfig();
+ const name = entity.getName();
+ const triggerPaths = config.triggerPaths;
+ return PathUtils.getFullNamesFromPropertyPaths(
+ Object.keys(triggerPaths),
+ name,
+ );
+ }
+
+ static getDynamicPaths(entity: IEntity) {
+ if (!isDynamicEntity(entity)) return [];
+ const reactivePaths = PathUtils.getReactivePaths(entity);
+ const triggerPaths = PathUtils.getTriggerPaths(entity);
+ const bindingPaths = PathUtils.getBindingPaths(entity);
+ return union(reactivePaths, triggerPaths, bindingPaths);
+ }
+ static getFullNamesFromPropertyPaths(paths: string[], parentName: string) {
+ return paths.map((path) => `${parentName}.${path}`);
+ }
+ static isDataPath(fullPath: string, entity: IEntity) {
+ if (!isWidgetEntity(entity) || !this.isDynamicLeaf(entity, fullPath))
+ return false;
+ const entityConfig = entity.getConfig();
+ const { propertyPath } = getEntityNameAndPropertyPath(fullPath);
+ return !(propertyPath in entityConfig.triggerPaths);
+ }
+ static getDataPaths(entity: IEntity) {
+ if (!isWidgetEntity(entity)) return [];
+ return PathUtils.getBindingPaths(entity);
+ }
+ static isDynamicLeaf(entity: IEntity, fullPropertyPath: string) {
+ const [entityName, ...propPathEls] = toPath(fullPropertyPath);
+ // Framework feature: Top level items are never leaves
+ if (entityName === fullPropertyPath) return false;
+
+ const entityConfig = entity.getConfig() as Record;
+ if (!entityConfig) return false;
+ const reactivePaths = entityConfig.reactivePaths as Record;
+
+ if (!isDynamicEntity(entity) || !entityConfig) return false;
+ const relativePropertyPath = convertPathToString(propPathEls);
+ return (
+ relativePropertyPath in reactivePaths ||
+ (isWidgetEntity(entity) &&
+ relativePropertyPath in entity.getConfig().triggerPaths)
+ );
+ }
+
+ static getAllPaths = (
+ records: any,
+ curKey = "",
+ result: Record = {},
+ ): Record => {
+ if (curKey) result[curKey] = true;
+ if (Array.isArray(records)) {
+ for (let i = 0; i < records.length; i++) {
+ const tempKey = curKey ? `${curKey}[${i}]` : `${i}`;
+ PathUtils.getAllPaths(records[i], tempKey, result);
+ }
+ } else if (isTrueObject(records)) {
+ for (const key of Object.keys(records)) {
+ const tempKey = curKey ? `${curKey}.${key}` : `${key}`;
+ PathUtils.getAllPaths(records[key], tempKey, result);
+ }
+ }
+ return result;
+ };
+}
diff --git a/app/client/src/plugins/Linting/utils/sortDependencies.ts b/app/client/src/plugins/Linting/utils/sortDependencies.ts
new file mode 100644
index 0000000000..9f68d12aee
--- /dev/null
+++ b/app/client/src/plugins/Linting/utils/sortDependencies.ts
@@ -0,0 +1,20 @@
+import type { TDependencies } from "entities/DependencyMap";
+import toposort from "toposort";
+
+export function sortDependencies(dependencyMap: TDependencies): Array {
+ // https://github.com/marcelklehr/toposort#sorting-dependencies
+ const edges: Array<[string, string | undefined]> = [];
+ dependencyMap.forEach((dependencies, path) => {
+ if (!dependencies.size) {
+ edges.push([path, undefined]);
+ } else {
+ dependencies.forEach((dependency) => edges.push([path, dependency]));
+ }
+ });
+
+ try {
+ return toposort(edges).reverse();
+ } catch (error) {
+ return [];
+ }
+}
diff --git a/app/client/src/workers/Linting/utils/sortLintingPathsByType.ts b/app/client/src/plugins/Linting/utils/sortLintingPathsByType.ts
similarity index 99%
rename from app/client/src/workers/Linting/utils/sortLintingPathsByType.ts
rename to app/client/src/plugins/Linting/utils/sortLintingPathsByType.ts
index b07b0c2611..3ed55f9f7b 100644
--- a/app/client/src/workers/Linting/utils/sortLintingPathsByType.ts
+++ b/app/client/src/plugins/Linting/utils/sortLintingPathsByType.ts
@@ -21,16 +21,18 @@ export default function sortLintingPathsByType(
const entity = unevalTree[entityName];
const entityConfig = configTree[entityName];
+ if (isJSAction(entity)) {
+ jsObjectPaths.add(fullPropertyPath);
+ continue;
+ }
+
// We are only interested in dynamic leaves
if (!isDynamicLeaf(unevalTree, fullPropertyPath, configTree)) continue;
if (isATriggerPath(entityConfig, propertyPath)) {
triggerPaths.add(fullPropertyPath);
continue;
}
- if (isJSAction(entity)) {
- jsObjectPaths.add(fullPropertyPath);
- continue;
- }
+
bindingPaths.add(fullPropertyPath);
}
diff --git a/app/client/src/preload-route-chunks.ts b/app/client/src/preload-route-chunks.ts
index 41f2f4432c..64be192133 100644
--- a/app/client/src/preload-route-chunks.ts
+++ b/app/client/src/preload-route-chunks.ts
@@ -16,8 +16,17 @@ declare global {
}
}
+// Preloading is disabled in LinkRelPreload_spec.js
+const isPreloadingDisabled =
+ new URL(window.location.href).searchParams.get("disableChunkPreload") ===
+ "true";
+
const currentMode = getModeForPathname(window.location.pathname);
-if (window.__APPSMITH_CHUNKS_TO_PRELOAD && currentMode) {
+if (
+ !isPreloadingDisabled &&
+ window.__APPSMITH_CHUNKS_TO_PRELOAD &&
+ currentMode
+) {
window.__APPSMITH_CHUNKS_TO_PRELOAD[currentMode]
// __webpack_public_path__ might be set on runtime when the CDN is used in EE
.map((url) => __webpack_public_path__ + url)
diff --git a/app/client/src/reducers/entityReducers/jsActionsReducer.tsx b/app/client/src/reducers/entityReducers/jsActionsReducer.tsx
index c09928d7c3..68dcc361ca 100644
--- a/app/client/src/reducers/entityReducers/jsActionsReducer.tsx
+++ b/app/client/src/reducers/entityReducers/jsActionsReducer.tsx
@@ -113,6 +113,18 @@ const jsActionsReducer = createReducer(initialState, {
};
return a;
}),
+ [ReduxActionTypes.UPDATE_JS_ACTION_BODY_INIT]: (
+ state: JSCollectionDataState,
+ action: ReduxAction<{ id: string; body: string }>,
+ ): JSCollectionDataState =>
+ state.map((a) => {
+ if (a.config.id === action.payload.id)
+ return {
+ ...a,
+ config: { ...a.config, body: action.payload.body },
+ };
+ return a;
+ }),
[ReduxActionErrorTypes.UPDATE_JS_ACTION_ERROR]: (
state: JSCollectionDataState,
action: ReduxAction<{ data: JSCollection }>,
diff --git a/app/client/src/sagas/EvalWorkerActionSagas.ts b/app/client/src/sagas/EvalWorkerActionSagas.ts
index 819b4b2a2d..afd84422ec 100644
--- a/app/client/src/sagas/EvalWorkerActionSagas.ts
+++ b/app/client/src/sagas/EvalWorkerActionSagas.ts
@@ -28,16 +28,14 @@ import type {
} from "workers/Evaluation/types";
import isEmpty from "lodash/isEmpty";
import type { UnEvalTree } from "entities/DataTree/dataTreeFactory";
-
+import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
+import type { LintTreeSagaRequestData } from "plugins/Linting/types";
+import AnalyticsUtil from "utils/AnalyticsUtil";
export type UpdateDataTreeMessageData = {
workerResponse: EvalTreeResponseData;
unevalTree: UnEvalTree;
};
-import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
-import type { LintTreeSagaRequestData } from "workers/Linting/types";
-import AnalyticsUtil from "utils/AnalyticsUtil";
-
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel) {
while (true) {
const request: TMessage = yield take(listenerChannel);
@@ -48,20 +46,11 @@ export function* handleEvalWorkerRequestSaga(listenerChannel: Channel) {
export function* lintTreeActionHandler(message: any) {
const { body } = message;
const { data } = body;
- const {
- asyncJSFunctionsInDataFields,
- configTree,
- jsPropertiesState,
- pathsToLint: lintOrder,
- unevalTree,
- } = data as LintTreeSagaRequestData;
+ const { configTree, unevalTree } = data as LintTreeSagaRequestData;
yield put({
type: ReduxActionTypes.LINT_TREE,
payload: {
- pathsToLint: lintOrder,
unevalTree,
- jsPropertiesState,
- asyncJSFunctionsInDataFields,
configTree,
},
});
diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts
index 25c660349b..d04e61a9d5 100644
--- a/app/client/src/sagas/EvaluationsSaga.ts
+++ b/app/client/src/sagas/EvaluationsSaga.ts
@@ -1,4 +1,4 @@
-import type { ActionPattern } from "redux-saga/effects";
+import type { ActionPattern, CallEffect, ForkEffect } from "redux-saga/effects";
import {
actionChannel,
all,
@@ -37,13 +37,15 @@ import PerformanceTracker, {
import * as Sentry from "@sentry/react";
import type { Action } from "redux";
import {
- EVALUATE_REDUX_ACTIONS,
+ EVAL_AND_LINT_REDUX_ACTIONS,
FIRST_EVAL_REDUX_ACTIONS,
setDependencyMap,
setEvaluatedTree,
- shouldLint,
+ shouldForceEval,
shouldLog,
- shouldProcessBatchedAction,
+ shouldProcessAction,
+ shouldTriggerEvaluation,
+ shouldTriggerLinting,
} from "actions/evaluationActions";
import ConfigTreeActions from "utils/configTree";
import {
@@ -87,7 +89,7 @@ import type {
WidgetEntityConfig,
} from "entities/DataTree/dataTreeFactory";
-import { lintWorker } from "./LintingSagas";
+import { initiateLinting, lintWorker } from "./LintingSagas";
import type {
EvalTreeRequestData,
EvalTreeResponseData,
@@ -105,6 +107,8 @@ export const evalWorker = new GracefulWorkerService(
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
{
type: "module",
+ // Note: the `Worker` part of the name is slightly important – LinkRelPreload_spec.js
+ // relies on it to find workers in the list of all requests.
name: "evalWorker",
},
),
@@ -230,17 +234,15 @@ export function* updateDataTreeHandler(
* yield call(evaluateTreeSaga, postEvalActions, shouldReplay, requiresLinting, forceEvaluation)
*/
export function* evaluateTreeSaga(
+ unEvalAndConfigTree: ReturnType,
postEvalActions?: Array,
shouldReplay = true,
- requiresLinting = false,
forceEvaluation = false,
requiresLogging = false,
) {
const allActionValidationConfig: ReturnType<
typeof getAllActionValidationConfig
> = yield select(getAllActionValidationConfig);
- const unEvalAndConfigTree: ReturnType =
- yield select(getUnevaluatedDataTree);
const unevalTree = unEvalAndConfigTree.unEvalTree;
const widgets: ReturnType = yield select(getWidgets);
const metaWidgets: ReturnType = yield select(
@@ -250,8 +252,6 @@ export function* evaluateTreeSaga(
getSelectedAppTheme,
);
const appMode: ReturnType = yield select(getAppMode);
-
- const isEditMode = appMode === APP_MODE.EDIT;
const toPrintConfigTree = unEvalAndConfigTree.configTree;
log.debug({ unevalTree, configTree: toPrintConfigTree });
PerformanceTracker.startAsyncTracking(
@@ -265,7 +265,6 @@ export function* evaluateTreeSaga(
theme,
shouldReplay,
allActionValidationConfig,
- requiresLinting: isEditMode && requiresLinting,
forceEvaluation,
metaWidgets,
appMode,
@@ -464,7 +463,7 @@ function evalQueueBuffer() {
const resp = collectedPostEvalActions;
collectedPostEvalActions = [];
canTake = false;
- return { postEvalActions: resp, type: "BUFFERED_ACTION" };
+ return { postEvalActions: resp, type: ReduxActionTypes.BUFFERED_ACTION };
}
};
const flush = () => {
@@ -476,7 +475,7 @@ function evalQueueBuffer() {
};
const put = (action: EvaluationReduxAction) => {
- if (!shouldProcessBatchedAction(action)) {
+ if (!shouldProcessAction(action)) {
return;
}
canTake = true;
@@ -522,6 +521,58 @@ function getPostEvalActions(
return postEvalActions;
}
+function* evalAndLintingHandler(
+ isBlockingCall = true,
+ action: ReduxAction,
+ options: Partial<{
+ shouldReplay: boolean;
+ forceEvaluation: boolean;
+ requiresLogging: boolean;
+ }>,
+) {
+ const { forceEvaluation, requiresLogging, shouldReplay } = options;
+ const appMode: ReturnType = yield select(getAppMode);
+
+ const requiresLinting =
+ appMode === APP_MODE.EDIT && shouldTriggerLinting(action);
+
+ const requiresEval = shouldTriggerEvaluation(action);
+ log.debug({
+ action,
+ triggeredLinting: requiresLinting,
+ triggeredEvaluation: requiresEval,
+ });
+
+ if (!requiresEval && !requiresLinting) return;
+
+ // Generate all the data needed for both eval and linting
+ const unEvalAndConfigTree: ReturnType =
+ yield select(getUnevaluatedDataTree);
+ const postEvalActions = getPostEvalActions(action);
+ const fn: (...args: unknown[]) => CallEffect | ForkEffect =
+ isBlockingCall ? call : fork;
+
+ const effects = [];
+
+ if (requiresEval) {
+ effects.push(
+ fn(
+ evaluateTreeSaga,
+ unEvalAndConfigTree,
+ postEvalActions,
+ shouldReplay,
+ forceEvaluation,
+ requiresLogging,
+ ),
+ );
+ }
+ if (requiresLinting) {
+ effects.push(fn(initiateLinting, unEvalAndConfigTree, forceEvaluation));
+ }
+
+ yield all(effects);
+}
+
function* evaluationChangeListenerSaga(): any {
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
@@ -536,13 +587,15 @@ function* evaluationChangeListenerSaga(): any {
yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
- const initAction: {
- type: ReduxActionType;
- postEvalActions: Array>;
- } = yield take(FIRST_EVAL_REDUX_ACTIONS);
- yield fork(evaluateTreeSaga, initAction.postEvalActions, false, true, false);
+ const initAction: EvaluationReduxAction = yield take(
+ FIRST_EVAL_REDUX_ACTIONS,
+ );
+ yield fork(evalAndLintingHandler, false, initAction, {
+ shouldReplay: false,
+ forceEvaluation: false,
+ });
const evtActionChannel: ActionPattern> = yield actionChannel(
- EVALUATE_REDUX_ACTIONS,
+ EVAL_AND_LINT_REDUX_ACTIONS,
evalQueueBuffer(),
);
while (true) {
@@ -550,18 +603,11 @@ function* evaluationChangeListenerSaga(): any {
evtActionChannel,
);
- if (shouldProcessBatchedAction(action)) {
- const postEvalActions = getPostEvalActions(action);
-
- yield call(
- evaluateTreeSaga,
- postEvalActions,
- get(action, "payload.shouldReplay"),
- shouldLint(action),
- false,
- shouldLog(action),
- );
- }
+ yield call(evalAndLintingHandler, true, action, {
+ shouldReplay: get(action, "payload.shouldReplay"),
+ forceEvaluation: shouldForceEval(action),
+ requiresLogging: shouldLog(action),
+ });
}
}
diff --git a/app/client/src/sagas/JSLibrarySaga.ts b/app/client/src/sagas/JSLibrarySaga.ts
index 1ecc82b11b..e0c46a2ea8 100644
--- a/app/client/src/sagas/JSLibrarySaga.ts
+++ b/app/client/src/sagas/JSLibrarySaga.ts
@@ -24,7 +24,7 @@ import { getCurrentApplicationId } from "selectors/editorSelectors";
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions";
import { validateResponse } from "./ErrorSagas";
-import { evaluateTreeSaga, EvalWorker } from "./EvaluationsSaga";
+import { EvalWorker } from "./EvaluationsSaga";
import log from "loglevel";
import { APP_MODE } from "entities/App";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
@@ -184,9 +184,6 @@ export function* installLibrarySaga(lib: Partial) {
},
});
- //TODO: Check if we could avoid this.
- yield call(evaluateTreeSaga, [], false, true, true);
-
yield put({
type: ReduxActionTypes.INSTALL_LIBRARY_SUCCESS,
payload: {
@@ -271,8 +268,6 @@ function* uninstallLibrarySaga(action: ReduxAction) {
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,
diff --git a/app/client/src/sagas/LintingSagas.ts b/app/client/src/sagas/LintingSagas.ts
index ec367c39d9..b69066248d 100644
--- a/app/client/src/sagas/LintingSagas.ts
+++ b/app/client/src/sagas/LintingSagas.ts
@@ -4,47 +4,46 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { APP_MODE } from "entities/App";
import { call, put, select, takeEvery } from "redux-saga/effects";
import { getAppMode } from "selectors/entitiesSelector";
-import { GracefulWorkerService } from "utils/WorkerUtil";
import type { TJSLibrary } from "workers/common/JSLibrary";
-import type {
- LintTreeRequest,
- LintTreeResponse,
- LintTreeSagaRequestData,
-} from "workers/Linting/types";
-import { LINT_WORKER_ACTIONS } from "workers/Linting/types";
import { logLatestLintPropertyErrors } from "./PostLintingSagas";
import { getAppsmithConfigs } from "@appsmith/configs";
import type { AppState } from "@appsmith/reducers";
import type { LintError } from "utils/DynamicBindingUtils";
-import { get, set, union } from "lodash";
+import { get, set, uniq } from "lodash";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
+import type {
+ LintTreeRequestPayload,
+ LintTreeResponse,
+ LintTreeSagaRequestData,
+} from "plugins/Linting/types";
+import type { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
+import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
+import { Linter } from "plugins/Linting/Linter";
+import log from "loglevel";
+import { getFixedTimeDifference } from "workers/common/DataTreeEvaluator/utils";
const APPSMITH_CONFIGS = getAppsmithConfigs();
-export const lintWorker = new GracefulWorkerService(
- new Worker(new URL("../workers/Linting/lint.worker.ts", import.meta.url), {
- type: "module",
- name: "lintWorker",
- }),
-);
+export const lintWorker = new Linter({ useWorker: true });
-function* updateLintGlobals(action: ReduxAction) {
+function* updateLintGlobals(
+ action: ReduxAction<{ add?: boolean; libs: 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,
- );
+ yield call(lintWorker.updateJSLibraryGlobals, action.payload);
}
-function* getValidOldJSCollectionLintErrors(
- jsEntities: string[],
+function* updateOldJSCollectionLintErrors(
+ lintedJSPaths: string[],
errors: LintErrorsStore,
jsObjectsState: TJSPropertiesState,
) {
+ const jsEntities = uniq(
+ lintedJSPaths.map((path) => getEntityNameAndPropertyPath(path).entityName),
+ );
const updatedJSCollectionLintErrors: LintErrorsStore = {};
for (const jsObjectName of jsEntities) {
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
@@ -57,23 +56,16 @@ function* getValidOldJSCollectionLintErrors(
[] as LintError[],
);
- const newJSBodyLintErrorsOriginalPaths = newJSBodyLintErrors.reduce(
- (paths, currentError) => {
- if (currentError.originalPath)
- return union(paths, [currentError.originalPath]);
- return paths;
- },
- [] as string[],
- );
-
const jsObjectState = get(jsObjectsState, jsObjectName, {});
- const jsObjectProperties = Object.keys(jsObjectState);
+ const jsObjectProperties = Object.keys(jsObjectState).map(
+ (propertyName) => `${jsObjectName}.${propertyName}`,
+ );
const filteredOldJsObjectBodyLintErrors = oldJsBodyLintErrors.filter(
(lintError) =>
lintError.originalPath &&
- lintError.originalPath in jsObjectProperties &&
- !(lintError.originalPath in newJSBodyLintErrorsOriginalPaths),
+ jsObjectProperties.includes(lintError.originalPath) &&
+ !lintedJSPaths.includes(lintError.originalPath),
);
const updatedLintErrors = [
...filteredOldJsObjectBodyLintErrors,
@@ -84,41 +76,27 @@ function* getValidOldJSCollectionLintErrors(
return updatedJSCollectionLintErrors;
}
-export function* lintTreeSaga(action: ReduxAction) {
- const {
- asyncJSFunctionsInDataFields,
- configTree,
- jsPropertiesState,
- pathsToLint,
- unevalTree,
- } = action.payload;
- // only perform lint operations in edit mode
- const appMode: APP_MODE = yield select(getAppMode);
- if (appMode !== APP_MODE.EDIT) return;
+export function* lintTreeSaga(payload: LintTreeSagaRequestData) {
+ const { configTree, forceLinting, unevalTree } = payload;
- const lintTreeRequestData: LintTreeRequest = {
- pathsToLint,
+ const lintTreeRequestData: LintTreeRequestPayload = {
unevalTree,
- jsPropertiesState,
configTree,
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
- asyncJSFunctionsInDataFields,
+ forceLinting,
};
- const { errors, updatedJSEntities }: LintTreeResponse = yield call(
- lintWorker.request,
- LINT_WORKER_ACTIONS.LINT_TREE,
- lintTreeRequestData,
- );
+ const { errors, jsPropertiesState, lintedJSPaths }: LintTreeResponse =
+ yield call(lintWorker.lintTree, lintTreeRequestData);
- const oldJSCollectionLintErrors: LintErrorsStore =
- yield getValidOldJSCollectionLintErrors(
- updatedJSEntities,
+ const updatedOldJSCollectionLintErrors: LintErrorsStore =
+ yield updateOldJSCollectionLintErrors(
+ lintedJSPaths,
errors,
jsPropertiesState,
);
- const updatedErrors = { ...errors, ...oldJSCollectionLintErrors };
+ const updatedErrors = { ...errors, ...updatedOldJSCollectionLintErrors };
yield put(setLintingErrors(updatedErrors));
yield call(logLatestLintPropertyErrors, {
@@ -127,7 +105,23 @@ export function* lintTreeSaga(action: ReduxAction) {
});
}
+export function* initiateLinting(
+ unEvalAndConfigTree: ReturnType,
+ forceLinting: boolean,
+) {
+ const lintingStartTime = performance.now();
+ const { configTree, unEvalTree: unevalTree } = unEvalAndConfigTree;
+
+ yield call(lintTreeSaga, {
+ unevalTree,
+ configTree,
+ forceLinting,
+ });
+ log.debug({
+ lintTime: getFixedTimeDifference(performance.now(), lintingStartTime),
+ });
+}
+
export default function* lintTreeSagaWatcher() {
yield takeEvery(ReduxActionTypes.UPDATE_LINT_GLOBALS, updateLintGlobals);
- yield takeEvery(ReduxActionTypes.LINT_TREE, lintTreeSaga);
}
diff --git a/app/client/src/utils/autocomplete/TernWorkerService.ts b/app/client/src/utils/autocomplete/TernWorkerService.ts
index e92a9fcc6d..771441d4e6 100644
--- a/app/client/src/utils/autocomplete/TernWorkerService.ts
+++ b/app/client/src/utils/autocomplete/TernWorkerService.ts
@@ -5,6 +5,8 @@ import { TernWorkerAction } from "./types";
const ternWorker = new Worker(
new URL("../../workers/Tern/tern.worker.ts", import.meta.url),
{
+ // Note: the `Worker` part of the name is slightly important – LinkRelPreload_spec.js
+ // relies on it to find workers in the list of all requests.
name: "TernWorker",
type: "module",
},
diff --git a/app/client/src/workers/Evaluation/JSObject/index.ts b/app/client/src/workers/Evaluation/JSObject/index.ts
index a08f90fe0c..8e1bce592b 100644
--- a/app/client/src/workers/Evaluation/JSObject/index.ts
+++ b/app/client/src/workers/Evaluation/JSObject/index.ts
@@ -64,7 +64,7 @@ export const getUpdatedLocalUnEvalTreeAfterJSUpdates = (
return localUnEvalTree;
};
-const regex = new RegExp(/^export default[\s]*?({[\s\S]*?})/);
+export const validJSBodyRegex = new RegExp(/^export default[\s]*?({[\s\S]*?})/);
/**
* Here we parse the JSObject and then determine
@@ -86,7 +86,7 @@ export function saveResolvedFunctionsAndJSUpdates(
entityName: string,
) {
jsPropertiesState.delete(entityName);
- const correctFormat = regex.test(entity.body);
+ const correctFormat = validJSBodyRegex.test(entity.body);
const isEmptyBody = entity.body.trim() === "";
if (correctFormat || isEmptyBody) {
diff --git a/app/client/src/workers/Evaluation/handlers/evalTree.ts b/app/client/src/workers/Evaluation/handlers/evalTree.ts
index fb3ca3f45a..450689d8f5 100644
--- a/app/client/src/workers/Evaluation/handlers/evalTree.ts
+++ b/app/client/src/workers/Evaluation/handlers/evalTree.ts
@@ -1,7 +1,7 @@
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
import type ReplayEntity from "entities/Replay";
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
-import { isEmpty, union } from "lodash";
+import { isEmpty } from "lodash";
import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
import type { JSUpdate } from "utils/JSPaneUtils";
@@ -21,13 +21,9 @@ import type {
import { clearAllIntervals } from "../fns/overrides/interval";
import JSObjectCollection from "workers/Evaluation/JSObject/Collection";
import { setEvalContext } from "../evaluate";
-import type { TJSPropertiesState } from "../JSObject/jsPropertiesState";
-import { jsPropertiesState } from "../JSObject/jsPropertiesState";
import { asyncJsFunctionInDataFields } from "../JSObject/asyncJSFunctionBoundToDataField";
-import type { LintTreeSagaRequestData } from "workers/Linting/types";
-import { WorkerMessenger } from "../fns/utils/Messenger";
-import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
import { getJSVariableCreatedEvents } from "../JSObject/JSVariableEvents";
+
export let replayMap: Record> | undefined;
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
export const CANVAS = "canvas";
@@ -35,7 +31,6 @@ export const CANVAS = "canvas";
export default function (request: EvalWorkerSyncRequest) {
const { data } = request;
let evalOrder: string[] = [];
- let lintOrder: string[] = [];
let jsUpdates: Record = {};
let unEvalUpdates: DataTreeDiff[] = [];
let nonDynamicFieldValidationOrder: string[] = [];
@@ -55,7 +50,6 @@ export default function (request: EvalWorkerSyncRequest) {
appMode,
forceEvaluation,
metaWidgets,
- requiresLinting,
shouldReplay,
theme,
unevalTree: __unevalTree__,
@@ -81,26 +75,8 @@ export default function (request: EvalWorkerSyncRequest) {
configTree,
);
evalOrder = setupFirstTreeResponse.evalOrder;
- lintOrder = union(
- setupFirstTreeResponse.lintOrder,
- jsPropertiesState.getUpdatedJSProperties(),
- );
jsUpdates = setupFirstTreeResponse.jsUpdates;
- initiateLinting({
- lintOrder,
- unevalTree: makeEntityConfigsAsObjProperties(
- dataTreeEvaluator.oldUnEvalTree,
- {
- sanitizeDataTree: false,
- },
- ),
- requiresLinting,
- jsPropertiesState: jsPropertiesState.getMap(),
- asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
- configTree: dataTreeEvaluator.oldConfigTree,
- });
-
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
@@ -131,26 +107,8 @@ export default function (request: EvalWorkerSyncRequest) {
);
isCreateFirstTree = true;
evalOrder = setupFirstTreeResponse.evalOrder;
- lintOrder = union(
- setupFirstTreeResponse.lintOrder,
- jsPropertiesState.getUpdatedJSProperties(),
- );
jsUpdates = setupFirstTreeResponse.jsUpdates;
- initiateLinting({
- lintOrder,
- unevalTree: makeEntityConfigsAsObjProperties(
- dataTreeEvaluator.oldUnEvalTree,
- {
- sanitizeDataTree: false,
- },
- ),
- requiresLinting,
- jsPropertiesState: jsPropertiesState.getMap(),
- asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
- configTree: dataTreeEvaluator.oldConfigTree,
- });
-
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
setEvalContext({
@@ -179,28 +137,11 @@ export default function (request: EvalWorkerSyncRequest) {
);
evalOrder = setupUpdateTreeResponse.evalOrder;
- lintOrder = union(
- setupUpdateTreeResponse.lintOrder,
- jsPropertiesState.getUpdatedJSProperties(),
- );
jsUpdates = setupUpdateTreeResponse.jsUpdates;
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor;
isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded;
- initiateLinting({
- lintOrder,
- unevalTree: makeEntityConfigsAsObjProperties(
- dataTreeEvaluator.oldUnEvalTree,
- {
- sanitizeDataTree: false,
- },
- ),
- requiresLinting,
- jsPropertiesState: jsPropertiesState.getMap(),
- asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
- configTree: dataTreeEvaluator.oldConfigTree,
- });
nonDynamicFieldValidationOrder =
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
@@ -291,34 +232,3 @@ export function clearCache() {
JSObjectCollection.clear();
return true;
}
-
-interface initiateLintingProps {
- asyncJSFunctionsInDataFields: DependencyMap;
- lintOrder: string[];
- unevalTree: DataTree;
- requiresLinting: boolean;
- jsPropertiesState: TJSPropertiesState;
- configTree: ConfigTree;
-}
-
-export function initiateLinting({
- asyncJSFunctionsInDataFields,
- configTree,
- jsPropertiesState,
- lintOrder,
- requiresLinting,
- unevalTree,
-}: initiateLintingProps) {
- const data = {
- pathsToLint: lintOrder,
- unevalTree,
- jsPropertiesState,
- asyncJSFunctionsInDataFields,
- configTree,
- } as LintTreeSagaRequestData;
- if (!requiresLinting) return;
- WorkerMessenger.ping({
- data,
- method: MAIN_THREAD_ACTION.LINT_TREE,
- });
-}
diff --git a/app/client/src/workers/Evaluation/types.ts b/app/client/src/workers/Evaluation/types.ts
index 117af9a838..ee0f38d051 100644
--- a/app/client/src/workers/Evaluation/types.ts
+++ b/app/client/src/workers/Evaluation/types.ts
@@ -36,7 +36,6 @@ export interface EvalTreeRequestData {
allActionValidationConfig: {
[actionId: string]: ActionValidationConfigMap;
};
- requiresLinting: boolean;
forceEvaluation: boolean;
metaWidgets: MetaWidgetsReduxState;
appMode: APP_MODE | undefined;
diff --git a/app/client/src/workers/Linting/lint.worker.ts b/app/client/src/workers/Linting/lint.worker.ts
deleted file mode 100644
index 574733c5b5..0000000000
--- a/app/client/src/workers/Linting/lint.worker.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { isEqual } from "lodash";
-import { WorkerErrorTypes } from "@appsmith/workers/common/types";
-import { JSLibraries } from "workers/common/JSLibrary";
-import { resetJSLibraries } from "workers/common/JSLibrary/resetJSLibraries";
-import type {
- LintWorkerRequest,
- LintTreeResponse,
- LintTreeRequest,
-} from "./types";
-import { LINT_WORKER_ACTIONS } from "./types";
-import type { TMessage } from "utils/MessageUtil";
-import { MessageType, sendMessage } from "utils/MessageUtil";
-import { getlintErrorsFromTree } from ".";
-
-function messageEventListener(fn: typeof eventRequestHandler) {
- return (event: MessageEvent>) => {
- 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({ method, requestData: data });
- const endTime = performance.now();
- if (!responseData) return;
-
- try {
- 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);
- sendMessage.call(self, {
- messageId,
- messageType: MessageType.RESPONSE,
- body: {
- data: {
- errors: [
- {
- type: WorkerErrorTypes.CLONE_ERROR,
- message: (e as Error)?.message,
- },
- ],
- },
- timeTaken: (endTime - startTime).toFixed(2),
- },
- });
- }
- };
-}
-
-function eventRequestHandler({
- method,
- requestData,
-}: {
- method: LINT_WORKER_ACTIONS;
- requestData: any;
-}): LintTreeResponse | unknown {
- switch (method) {
- case LINT_WORKER_ACTIONS.LINT_TREE: {
- const lintTreeResponse: LintTreeResponse = {
- errors: {},
- updatedJSEntities: [],
- };
- try {
- const {
- asyncJSFunctionsInDataFields,
- cloudHosting,
- configTree,
- jsPropertiesState,
- pathsToLint,
- unevalTree: unEvalTree,
- } = requestData as LintTreeRequest;
- const { errors: lintErrors, updatedJSEntities } = getlintErrorsFromTree(
- {
- pathsToLint,
- unEvalTree,
- jsPropertiesState,
- cloudHosting,
- asyncJSFunctionsInDataFields,
- configTree,
- },
- );
-
- lintTreeResponse.errors = lintErrors;
- lintTreeResponse.updatedJSEntities = updatedJSEntities;
- } 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);
- }
- }
-}
-
-self.onmessage = messageEventListener(eventRequestHandler);
diff --git a/app/client/src/workers/common/DependencyMap/utils.ts b/app/client/src/workers/common/DependencyMap/utils.ts
index 8150126909..9dd63f2abe 100644
--- a/app/client/src/workers/common/DependencyMap/utils.ts
+++ b/app/client/src/workers/common/DependencyMap/utils.ts
@@ -240,8 +240,8 @@ export function listEntityDependencies(
});
}
const widgetDependencies = addWidgetPropertyDependencies({
- entity: widgetConfig,
- entityName,
+ widgetConfig,
+ widgetName: entityName,
});
dependencies = {