feat: Improve Linting performance (#23865)
## Description This PR introduces a new architecture, making evaluation and linting independent. <img width="500" alt="Screenshot 2023-07-04 at 17 24 40" src="https://github.com/appsmithorg/appsmith/assets/46670083/00b1eab9-cd79-4442-b51a-5345c2d6c4da"> In the previous architecture, one dependency graph was used to hold the relationship between entities in the application and subsequently, the "evaluation order" and "paths to lint" were generated. Although similar, the dependency graph required for evaluation and linting differ. For example, trigger fields should not depend on any other entity/entity path in the eval's dependency graph since they are not reactive. This is not the case for the linting dependency graph. ## Performance - This PR introduces "lint only" actions. These actions trigger linting, but not evaluation. For example, UPDATE_JS_ACTION_BODY_INIT (which is fired immediately after a user edits the body of a JS Object). Since linting fires without waiting for a successful update on the server, **response time decreases by 40%** (from 2s to 1.2s). - Reduction in time taken to generate paths requiring linting. <img width="715" alt="Screenshot 2023-07-04 at 18 10 52" src="https://github.com/appsmithorg/appsmith/assets/46670083/d73a4bfc-de73-4fa7-bdca-af1e5d8ce8a1"> #### PR fixes following issue(s) Fixes #23447 Fixes #23166 Fixes #24194 Fixes #23720 Fixes #23868 Fixes #21895 Latest DP: https://appsmith-r3f9e325p-get-appsmith.vercel.app/ #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing > #### How Has This Been Tested? - [x] Manual - [ ] Jest - [ ] Cypress > > #### Test Plan https://github.com/appsmithorg/appsmith/pull/23865#issuecomment-1606738633 > > #### Issues raised during DP testing https://github.com/appsmithorg/appsmith/pull/23865#issuecomment-1608779227 response: https://github.com/appsmithorg/appsmith/pull/23865#issuecomment-1619677033 > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest) - [x] Test plan has been peer reviewed by project stakeholders and other QA members - [x] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed --------- Co-authored-by: arunvjn <arun@appsmith.com> Co-authored-by: Ivan Akulov <mail@iamakulov.com>
This commit is contained in:
parent
1ab630f082
commit
e6f2dcacde
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 <link rel='preload'>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());
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<unknown>) => {
|
||||
if (
|
||||
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
|
||||
Array.isArray(action.payload)
|
||||
) {
|
||||
const batchedActionTypes = action.payload.map(
|
||||
(batchedAction) => batchedAction.type,
|
||||
);
|
||||
|
||||
export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => {
|
||||
return (
|
||||
_.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0
|
||||
shouldProcessAction(action) && EVALUATE_REDUX_ACTIONS.includes(action.type)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
export const shouldTriggerLinting = (action: ReduxAction<unknown>) => {
|
||||
return shouldProcessAction(action) && !!LINT_REDUX_ACTIONS[action.type];
|
||||
};
|
||||
|
||||
export function shouldLint(action: ReduxAction<unknown>) {
|
||||
export const getAllActionTypes = (action: ReduxAction<unknown>) => {
|
||||
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<unknown>) => {
|
||||
const actionTypes = getAllActionTypes(action);
|
||||
|
||||
return intersection(EVAL_AND_LINT_REDUX_ACTIONS, actionTypes).length > 0;
|
||||
};
|
||||
|
||||
export function shouldLog(action: ReduxAction<unknown>) {
|
||||
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<unknown>) => {
|
||||
return !!FORCE_EVAL_ACTIONS[action.type];
|
||||
};
|
||||
|
||||
export const EVAL_AND_LINT_REDUX_ACTIONS = union(
|
||||
EVALUATE_REDUX_ACTIONS,
|
||||
Object.keys(LINT_REDUX_ACTIONS),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export const generateDataTreeAction = (
|
|||
const { bindingPaths, reactivePaths } = getBindingAndReactivePathsOfAction(
|
||||
action.config,
|
||||
editorConfig,
|
||||
dynamicBindingPathList,
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
export type TDependencies = Map<string, Set<string>>;
|
||||
export default class DependencyMap {
|
||||
#nodes: Map<string, true>;
|
||||
#dependencies: Map<string, Set<string>>;
|
||||
#dependenciesInverse: Map<string, Set<string>>;
|
||||
#invalidDependencies: Map<string, Set<string>>;
|
||||
#invalidDependenciesInverse: Map<string, Set<string>>;
|
||||
#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<string>();
|
||||
const invalidDependencies = new Set<string>();
|
||||
|
||||
const currentNodeDependencies =
|
||||
this.#dependencies.get(node) || new Set<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
app/client/src/plugins/Linting/Linter.ts
Normal file
26
app/client/src/plugins/Linting/Linter.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
8
app/client/src/plugins/Linting/handlers/index.ts
Normal file
8
app/client/src/plugins/Linting/handlers/index.ts
Normal file
|
|
@ -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;
|
||||
352
app/client/src/plugins/Linting/handlers/lintService.ts
Normal file
352
app/client/src/plugins/Linting/handlers/lintService.ts
Normal file
|
|
@ -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<string> = [];
|
||||
const allNodes: Record<string, true> = entityTree.getAllPaths();
|
||||
const asyncJSFunctionsInDataFields: Record<string, string[]> = {};
|
||||
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<string, string[]> = {};
|
||||
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<string, true>);
|
||||
}
|
||||
|
||||
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<string>();
|
||||
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();
|
||||
|
|
@ -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;
|
||||
}
|
||||
114
app/client/src/plugins/Linting/lib/entity/EntityTree.ts
Normal file
114
app/client/src/plugins/Linting/lib/entity/EntityTree.ts
Normal file
|
|
@ -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<string, IEntity>();
|
||||
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<unknown>[] = [];
|
||||
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<string, true> => {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
309
app/client/src/plugins/Linting/lib/entity/index.ts
Normal file
309
app/client/src/plugins/Linting/lib/entity/index.ts
Normal file
|
|
@ -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<unknown>[] | 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<unknown>[] | 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<unknown>[] | 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<unknown>[] | 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<unknown>[] | 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<unknown>[] | 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());
|
||||
}
|
||||
|
|
@ -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<string>();
|
||||
const lintedJSPaths = new Set<string>();
|
||||
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),
|
||||
};
|
||||
}
|
||||
59
app/client/src/plugins/Linting/linters/index.ts
Normal file
59
app/client/src/plugins/Linting/linters/index.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
26
app/client/src/plugins/Linting/linters/worker.ts
Normal file
26
app/client/src/plugins/Linting/linters/worker.ts
Normal file
|
|
@ -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<TMessage<LintRequest>>) {
|
||||
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;
|
||||
|
|
@ -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<typeof createEvaluationContext>;
|
||||
}
|
||||
|
||||
export interface lintBindingPathProps {
|
||||
dynamicBinding: string;
|
||||
entity: DataTreeEntity;
|
||||
fullPropertyPath: string;
|
||||
globalData: ReturnType<typeof createEvaluationContext>;
|
||||
}
|
||||
|
||||
export interface getLintingErrorsProps {
|
||||
script: string;
|
||||
data: Record<string, unknown>;
|
||||
|
|
@ -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[];
|
||||
}
|
||||
68
app/client/src/plugins/Linting/utils/diffGenerator.ts
Normal file
68
app/client/src/plugins/Linting/utils/diffGenerator.ts
Normal file
|
|
@ -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<unknown>[] | 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<string, string> = {};
|
||||
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();
|
||||
170
app/client/src/plugins/Linting/utils/entityParser.ts
Normal file
170
app/client/src/plugins/Linting/utils/entityParser.ts
Normal file
|
|
@ -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<T extends DataTreeEntity, K extends DataTreeEntityConfig>(
|
||||
entity: T,
|
||||
entityConfig: K,
|
||||
): ParsedEntity<T>;
|
||||
parse<T extends TJSActionEntity, K extends JSActionEntityConfig>(
|
||||
entity: T,
|
||||
entityConfig: K,
|
||||
): ParsedEntity<T>;
|
||||
}
|
||||
|
||||
type TParsedJSEntity = Record<string, string> & {
|
||||
body: string;
|
||||
};
|
||||
|
||||
type TParsedJSEntityConfig = Record<string, TParsedJSProperty>;
|
||||
|
||||
export type ParsedJSCache = {
|
||||
parsedEntity: ParsedEntity<TJSActionEntity>;
|
||||
parsedEntityConfig: TParsedJSEntityConfig;
|
||||
};
|
||||
|
||||
export type ParsedEntity<T> = {
|
||||
parsedEntity: Partial<T>;
|
||||
parsedEntityConfig: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export class DefaultEntityParser implements EntityParser {
|
||||
parse<T extends DataTreeEntity>(entity: T) {
|
||||
return {
|
||||
parsedEntity: entity,
|
||||
parsedEntityConfig: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class JSLintEntityParser implements EntityParser {
|
||||
#parsedJSCache: ParsedEntity<TJSActionEntity> = {
|
||||
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<string, unknown> = {};
|
||||
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<string, never> }
|
||||
| { success: true; parsedObject: Record<string, TParsedJSProperty> } =
|
||||
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<string, TParsedJSProperty>, property) => {
|
||||
const updatedProperties = { ...acc, [property.key]: property };
|
||||
return updatedProperties;
|
||||
},
|
||||
{},
|
||||
),
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
export const jsLintEntityParser = new JSLintEntityParser();
|
||||
289
app/client/src/plugins/Linting/utils/getEntityDependencies.ts
Normal file
289
app/client/src/plugins/Linting/utils/getEntityDependencies.ts
Normal file
|
|
@ -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<string, TDependencyMap>).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<string, unknown>,
|
||||
) {
|
||||
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<string, unknown>,
|
||||
) {
|
||||
const { references } = extractIdentifierInfoFromCode(jsSnippet, 2);
|
||||
const prunedReferences = flatten(
|
||||
references.map((reference) => getPrunedReference(reference, tree)),
|
||||
);
|
||||
return uniq(prunedReferences);
|
||||
}
|
||||
|
||||
function getPrunedReference(
|
||||
reference: string,
|
||||
tree: Record<string, unknown>,
|
||||
): 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import type { Diff, DiffArray } from "deep-diff";
|
||||
import { isEmpty, partition } from "lodash";
|
||||
|
||||
export function groupDifferencesByType(differences: Diff<unknown>[]): {
|
||||
edits: Diff<unknown>[];
|
||||
additions: Diff<unknown>[];
|
||||
deletions: Diff<unknown>[];
|
||||
} {
|
||||
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<unknown>[]).reduce(
|
||||
(acc, currentDiff) => {
|
||||
if (!currentDiff.path) return acc;
|
||||
const { index, item, path } = currentDiff;
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...item,
|
||||
path: [...path, index],
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as Diff<unknown>[],
|
||||
);
|
||||
|
||||
const result = groupDifferencesByType(refinedChanges);
|
||||
|
||||
return {
|
||||
edits: edits.concat(result.edits),
|
||||
additions: additions.concat(result.additions),
|
||||
deletions: deletions.concat(result.deletions),
|
||||
};
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
12
app/client/src/plugins/Linting/utils/mergeMaps.ts
Normal file
12
app/client/src/plugins/Linting/utils/mergeMaps.ts
Normal file
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
104
app/client/src/plugins/Linting/utils/pathUtils.ts
Normal file
104
app/client/src/plugins/Linting/utils/pathUtils.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
if (!entityConfig) return false;
|
||||
const reactivePaths = entityConfig.reactivePaths as Record<string, unknown>;
|
||||
|
||||
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<string, true> = {},
|
||||
): Record<string, true> => {
|
||||
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;
|
||||
};
|
||||
}
|
||||
20
app/client/src/plugins/Linting/utils/sortDependencies.ts
Normal file
20
app/client/src/plugins/Linting/utils/sortDependencies.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { TDependencies } from "entities/DependencyMap";
|
||||
import toposort from "toposort";
|
||||
|
||||
export function sortDependencies(dependencyMap: TDependencies): Array<string> {
|
||||
// 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<string>(edges).reverse();
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }>,
|
||||
|
|
|
|||
|
|
@ -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<any>) {
|
||||
while (true) {
|
||||
const request: TMessage<any> = yield take(listenerChannel);
|
||||
|
|
@ -48,20 +46,11 @@ export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<typeof getUnevaluatedDataTree>,
|
||||
postEvalActions?: Array<AnyReduxAction>,
|
||||
shouldReplay = true,
|
||||
requiresLinting = false,
|
||||
forceEvaluation = false,
|
||||
requiresLogging = false,
|
||||
) {
|
||||
const allActionValidationConfig: ReturnType<
|
||||
typeof getAllActionValidationConfig
|
||||
> = yield select(getAllActionValidationConfig);
|
||||
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
|
||||
yield select(getUnevaluatedDataTree);
|
||||
const unevalTree = unEvalAndConfigTree.unEvalTree;
|
||||
const widgets: ReturnType<typeof getWidgets> = yield select(getWidgets);
|
||||
const metaWidgets: ReturnType<typeof getMetaWidgets> = yield select(
|
||||
|
|
@ -250,8 +252,6 @@ export function* evaluateTreeSaga(
|
|||
getSelectedAppTheme,
|
||||
);
|
||||
const appMode: ReturnType<typeof getAppMode> = 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<unknown | unknown[]>) => {
|
||||
if (!shouldProcessBatchedAction(action)) {
|
||||
if (!shouldProcessAction(action)) {
|
||||
return;
|
||||
}
|
||||
canTake = true;
|
||||
|
|
@ -522,6 +521,58 @@ function getPostEvalActions(
|
|||
return postEvalActions;
|
||||
}
|
||||
|
||||
function* evalAndLintingHandler(
|
||||
isBlockingCall = true,
|
||||
action: ReduxAction<unknown>,
|
||||
options: Partial<{
|
||||
shouldReplay: boolean;
|
||||
forceEvaluation: boolean;
|
||||
requiresLogging: boolean;
|
||||
}>,
|
||||
) {
|
||||
const { forceEvaluation, requiresLogging, shouldReplay } = options;
|
||||
const appMode: ReturnType<typeof getAppMode> = 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<typeof getUnevaluatedDataTree> =
|
||||
yield select(getUnevaluatedDataTree);
|
||||
const postEvalActions = getPostEvalActions(action);
|
||||
const fn: (...args: unknown[]) => CallEffect<unknown> | ForkEffect<unknown> =
|
||||
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<ReduxAction<unknown>>;
|
||||
} = yield take(FIRST_EVAL_REDUX_ACTIONS);
|
||||
yield fork(evaluateTreeSaga, initAction.postEvalActions, false, true, false);
|
||||
const initAction: EvaluationReduxAction<unknown> = yield take(
|
||||
FIRST_EVAL_REDUX_ACTIONS,
|
||||
);
|
||||
yield fork(evalAndLintingHandler, false, initAction, {
|
||||
shouldReplay: false,
|
||||
forceEvaluation: false,
|
||||
});
|
||||
const evtActionChannel: ActionPattern<Action<any>> = 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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TJSLibrary>) {
|
|||
},
|
||||
});
|
||||
|
||||
//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<TJSLibrary>) {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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<TJSLibrary>) {
|
||||
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<LintTreeSagaRequestData>) {
|
||||
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<LintTreeSagaRequestData>) {
|
|||
});
|
||||
}
|
||||
|
||||
export function* initiateLinting(
|
||||
unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree>,
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, ReplayEntity<any>> | 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<string, JSUpdate> = {};
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export interface EvalTreeRequestData {
|
|||
allActionValidationConfig: {
|
||||
[actionId: string]: ActionValidationConfigMap;
|
||||
};
|
||||
requiresLinting: boolean;
|
||||
forceEvaluation: boolean;
|
||||
metaWidgets: MetaWidgetsReduxState;
|
||||
appMode: APP_MODE | undefined;
|
||||
|
|
|
|||
|
|
@ -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<TMessage<LintWorkerRequest>>) => {
|
||||
const { messageType } = event.data;
|
||||
if (messageType !== MessageType.REQUEST) return;
|
||||
const { body, messageId } = event.data;
|
||||
const { data, method } = body;
|
||||
if (!method) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
const responseData = fn({ 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);
|
||||
|
|
@ -240,8 +240,8 @@ export function listEntityDependencies(
|
|||
});
|
||||
}
|
||||
const widgetDependencies = addWidgetPropertyDependencies({
|
||||
entity: widgetConfig,
|
||||
entityName,
|
||||
widgetConfig,
|
||||
widgetName: entityName,
|
||||
});
|
||||
|
||||
dependencies = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user