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 = {