@webstudio-is/sdk 0.142.0 → 0.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -579,6 +579,510 @@ var loadResource = async (resourceData) => {
579
579
  };
580
580
  }
581
581
  };
582
+
583
+ // src/expression.ts
584
+ import { parseExpressionAt } from "acorn";
585
+ import { simple } from "acorn-walk";
586
+ var lintExpression = ({
587
+ expression,
588
+ availableVariables = /* @__PURE__ */ new Set(),
589
+ allowAssignment = false
590
+ }) => {
591
+ const diagnostics = [];
592
+ const addError = (message) => {
593
+ return (node) => {
594
+ diagnostics.push({
595
+ from: node.start,
596
+ to: node.end,
597
+ severity: "error",
598
+ message
599
+ });
600
+ };
601
+ };
602
+ if (expression.trim().length === 0) {
603
+ return diagnostics;
604
+ }
605
+ try {
606
+ const root = parseExpressionAt(expression, 0, {
607
+ ecmaVersion: "latest",
608
+ // support parsing import to forbid explicitly
609
+ sourceType: "module"
610
+ });
611
+ simple(root, {
612
+ Identifier(node) {
613
+ if (availableVariables.has(node.name) === false) {
614
+ addError(`"${node.name}" is not available in the scope`)(node);
615
+ }
616
+ },
617
+ Literal() {
618
+ },
619
+ ArrayExpression() {
620
+ },
621
+ ObjectExpression() {
622
+ },
623
+ UnaryExpression() {
624
+ },
625
+ BinaryExpression() {
626
+ },
627
+ LogicalExpression() {
628
+ },
629
+ MemberExpression() {
630
+ },
631
+ ConditionalExpression() {
632
+ },
633
+ TemplateLiteral() {
634
+ },
635
+ ChainExpression() {
636
+ },
637
+ ParenthesizedExpression() {
638
+ },
639
+ AssignmentExpression(node) {
640
+ if (allowAssignment === false) {
641
+ addError("Assignment is supported only inside actions")(node);
642
+ return;
643
+ }
644
+ simple(node.left, {
645
+ Identifier(node2) {
646
+ if (availableVariables.has(node2.name) === false) {
647
+ addError(`"${node2.name}" is not available in the scope`)(node2);
648
+ }
649
+ }
650
+ });
651
+ },
652
+ // parser forbids to yield inside module
653
+ YieldExpression() {
654
+ },
655
+ ThisExpression: addError(`"this" keyword is not supported`),
656
+ FunctionExpression: addError("Functions are not supported"),
657
+ UpdateExpression: addError("Increment and decrement are not supported"),
658
+ CallExpression: addError("Functions are not supported"),
659
+ NewExpression: addError("Classes are not supported"),
660
+ SequenceExpression: addError(`Only single expression is supported`),
661
+ ArrowFunctionExpression: addError("Functions are not supported"),
662
+ TaggedTemplateExpression: addError("Tagged template is not supported"),
663
+ ClassExpression: addError("Classes are not supported"),
664
+ MetaProperty: addError("Imports are not supported"),
665
+ AwaitExpression: addError(`"await" keyword is not supported`),
666
+ ImportExpression: addError("Imports are not supported")
667
+ });
668
+ } catch (error) {
669
+ const castedError = error;
670
+ diagnostics.push({
671
+ from: castedError.pos,
672
+ to: castedError.pos,
673
+ severity: "error",
674
+ message: castedError.message
675
+ });
676
+ }
677
+ return diagnostics;
678
+ };
679
+ var isLiteralNode = (node) => {
680
+ if (node.type === "Literal") {
681
+ return true;
682
+ }
683
+ if (node.type === "ArrayExpression") {
684
+ return node.elements.every((node2) => {
685
+ if (node2 === null || node2.type === "SpreadElement") {
686
+ return false;
687
+ }
688
+ return isLiteralNode(node2);
689
+ });
690
+ }
691
+ if (node.type === "ObjectExpression") {
692
+ return node.properties.every((property) => {
693
+ if (property.type === "SpreadElement") {
694
+ return false;
695
+ }
696
+ const key = property.key;
697
+ const isIdentifierKey = key.type === "Identifier" && property.computed === false;
698
+ const isLiteralKey = key.type === "Literal";
699
+ return (isLiteralKey || isIdentifierKey) && isLiteralNode(property.value);
700
+ });
701
+ }
702
+ return false;
703
+ };
704
+ var isLiteralExpression = (expression) => {
705
+ try {
706
+ const node = parseExpressionAt(expression, 0, { ecmaVersion: "latest" });
707
+ return isLiteralNode(node);
708
+ } catch {
709
+ return false;
710
+ }
711
+ };
712
+ var getExpressionIdentifiers = (expression) => {
713
+ const identifiers = /* @__PURE__ */ new Set();
714
+ try {
715
+ const root = parseExpressionAt(expression, 0, { ecmaVersion: "latest" });
716
+ simple(root, {
717
+ Identifier: (node) => identifiers.add(node.name),
718
+ AssignmentExpression(node) {
719
+ simple(node.left, {
720
+ Identifier: (node2) => identifiers.add(node2.name)
721
+ });
722
+ }
723
+ });
724
+ } catch {
725
+ }
726
+ return identifiers;
727
+ };
728
+ var transpileExpression = ({
729
+ expression,
730
+ executable = false,
731
+ replaceVariable
732
+ }) => {
733
+ const root = parseExpressionAt(expression, 0, { ecmaVersion: "latest" });
734
+ const replacements = [];
735
+ const replaceIdentifier = (node, assignee) => {
736
+ const newName = replaceVariable?.(node.name, assignee);
737
+ if (newName) {
738
+ replacements.push([node.start, node.end, newName]);
739
+ }
740
+ };
741
+ simple(root, {
742
+ Identifier: (node) => replaceIdentifier(node, false),
743
+ AssignmentExpression(node) {
744
+ simple(node.left, {
745
+ Identifier: (node2) => replaceIdentifier(node2, true)
746
+ });
747
+ },
748
+ MemberExpression(node) {
749
+ if (executable === false || node.optional) {
750
+ return;
751
+ }
752
+ if (node.computed === false) {
753
+ const dotIndex = expression.indexOf(".", node.object.end);
754
+ replacements.push([dotIndex, dotIndex, "?"]);
755
+ }
756
+ if (node.computed === true) {
757
+ const dotIndex = expression.indexOf("[", node.object.end);
758
+ replacements.push([dotIndex, dotIndex, "?."]);
759
+ }
760
+ }
761
+ });
762
+ replacements.sort(([leftStart], [rightStart]) => rightStart - leftStart);
763
+ for (const [start, end, fragment] of replacements) {
764
+ const before = expression.slice(0, start);
765
+ const after = expression.slice(end);
766
+ expression = before + fragment + after;
767
+ }
768
+ return expression;
769
+ };
770
+ var dataSourceVariablePrefix = "$ws$dataSource$";
771
+ var encodeDataSourceVariable = (id) => {
772
+ const encoded = id.replaceAll("-", "__DASH__");
773
+ return `${dataSourceVariablePrefix}${encoded}`;
774
+ };
775
+ var decodeDataSourceVariable = (name) => {
776
+ if (name.startsWith(dataSourceVariablePrefix)) {
777
+ const encoded = name.slice(dataSourceVariablePrefix.length);
778
+ return encoded.replaceAll("__DASH__", "-");
779
+ }
780
+ return;
781
+ };
782
+ var generateExpression = ({
783
+ expression,
784
+ dataSources,
785
+ usedDataSources,
786
+ scope
787
+ }) => {
788
+ return transpileExpression({
789
+ expression,
790
+ executable: true,
791
+ replaceVariable: (identifier) => {
792
+ const depId = decodeDataSourceVariable(identifier);
793
+ const dep = depId ? dataSources.get(depId) : void 0;
794
+ if (dep) {
795
+ usedDataSources?.set(dep.id, dep);
796
+ return scope.getName(dep.id, dep.name);
797
+ }
798
+ }
799
+ });
800
+ };
801
+ var executeExpression = (expression) => {
802
+ try {
803
+ const fn = new Function(`return (${expression})`);
804
+ return fn();
805
+ } catch {
806
+ }
807
+ };
808
+
809
+ // src/forms-generator.ts
810
+ var generateFormsProperties = (props) => {
811
+ const formsProperties = /* @__PURE__ */ new Map();
812
+ for (const prop of props.values()) {
813
+ if (prop.type === "string") {
814
+ if (prop.name === "action" || prop.name === "method") {
815
+ let properties = formsProperties.get(prop.instanceId);
816
+ if (properties === void 0) {
817
+ properties = {};
818
+ }
819
+ properties[prop.name] = prop.value;
820
+ formsProperties.set(prop.instanceId, properties);
821
+ }
822
+ }
823
+ }
824
+ const entriesString = JSON.stringify(Array.from(formsProperties.entries()));
825
+ let generated = "";
826
+ generated += `type FormProperties = { method?: string, action?: string }
827
+ `;
828
+ generated += `export const formsProperties = new Map<string, FormProperties>(${entriesString})
829
+ `;
830
+ return generated;
831
+ };
832
+
833
+ // src/resources-generator.ts
834
+ var generateResourcesLoader = ({
835
+ scope,
836
+ page,
837
+ dataSources,
838
+ resources
839
+ }) => {
840
+ let generatedOutput = "";
841
+ let generatedLoaders = "";
842
+ let hasResources = false;
843
+ const usedDataSources = /* @__PURE__ */ new Map();
844
+ for (const dataSource of dataSources.values()) {
845
+ if (dataSource.type === "resource") {
846
+ const resource = resources.get(dataSource.resourceId);
847
+ if (resource === void 0) {
848
+ continue;
849
+ }
850
+ hasResources = true;
851
+ const resourceName = scope.getName(resource.id, dataSource.name);
852
+ generatedOutput += `${resourceName},
853
+ `;
854
+ generatedLoaders += `loadResource({
855
+ `;
856
+ generatedLoaders += `id: "${resource.id}",
857
+ `;
858
+ generatedLoaders += `name: ${JSON.stringify(resource.name)},
859
+ `;
860
+ const url = generateExpression({
861
+ expression: resource.url,
862
+ dataSources,
863
+ usedDataSources,
864
+ scope
865
+ });
866
+ generatedLoaders += `url: ${url},
867
+ `;
868
+ generatedLoaders += `method: "${resource.method}",
869
+ `;
870
+ generatedLoaders += `headers: [
871
+ `;
872
+ for (const header of resource.headers) {
873
+ const value = generateExpression({
874
+ expression: header.value,
875
+ dataSources,
876
+ usedDataSources,
877
+ scope
878
+ });
879
+ generatedLoaders += `{ name: "${header.name}", value: ${value} },
880
+ `;
881
+ }
882
+ generatedLoaders += `],
883
+ `;
884
+ if (resource.body !== void 0 && resource.body.length > 0) {
885
+ const body = generateExpression({
886
+ expression: resource.body,
887
+ dataSources,
888
+ usedDataSources,
889
+ scope
890
+ });
891
+ generatedLoaders += `body: ${body},
892
+ `;
893
+ }
894
+ generatedLoaders += `}),
895
+ `;
896
+ }
897
+ }
898
+ let generatedVariables = "";
899
+ for (const dataSource of usedDataSources.values()) {
900
+ if (dataSource.type === "variable") {
901
+ const name = scope.getName(dataSource.id, dataSource.name);
902
+ const value = JSON.stringify(dataSource.value.value);
903
+ generatedVariables += `let ${name} = ${value}
904
+ `;
905
+ }
906
+ if (dataSource.type === "parameter") {
907
+ if (dataSource.id !== page.systemDataSourceId) {
908
+ continue;
909
+ }
910
+ const name = scope.getName(dataSource.id, dataSource.name);
911
+ generatedVariables += `const ${name} = _props.system
912
+ `;
913
+ }
914
+ }
915
+ let generated = "";
916
+ generated += `import { loadResource, type System } from "@webstudio-is/sdk";
917
+ `;
918
+ generated += `export const loadResources = async (_props: { system: System }) => {
919
+ `;
920
+ generated += generatedVariables;
921
+ if (hasResources) {
922
+ generated += `const [
923
+ `;
924
+ generated += generatedOutput;
925
+ generated += `] = await Promise.all([
926
+ `;
927
+ generated += generatedLoaders;
928
+ generated += `])
929
+ `;
930
+ }
931
+ generated += `return {
932
+ `;
933
+ generated += generatedOutput;
934
+ generated += `} as Record<string, unknown>
935
+ `;
936
+ generated += `}
937
+ `;
938
+ return generated;
939
+ };
940
+
941
+ // src/page-meta-generator.ts
942
+ var generatePageMeta = ({
943
+ globalScope,
944
+ page,
945
+ dataSources
946
+ }) => {
947
+ const localScope = createScope(["system", "resources"]);
948
+ const usedDataSources = /* @__PURE__ */ new Map();
949
+ const titleExpression = generateExpression({
950
+ expression: page.title,
951
+ dataSources,
952
+ usedDataSources,
953
+ scope: localScope
954
+ });
955
+ const descriptionExpression = generateExpression({
956
+ expression: page.meta.description ?? "undefined",
957
+ dataSources,
958
+ usedDataSources,
959
+ scope: localScope
960
+ });
961
+ const excludePageFromSearchExpression = generateExpression({
962
+ expression: page.meta.excludePageFromSearch ?? "undefined",
963
+ dataSources,
964
+ usedDataSources,
965
+ scope: localScope
966
+ });
967
+ const languageExpression = generateExpression({
968
+ expression: page.meta.language ?? "undefined",
969
+ dataSources,
970
+ usedDataSources,
971
+ scope: localScope
972
+ });
973
+ const socialImageAssetIdExpression = JSON.stringify(
974
+ page.meta.socialImageAssetId
975
+ );
976
+ const socialImageUrlExpression = generateExpression({
977
+ expression: page.meta.socialImageUrl ?? "undefined",
978
+ dataSources,
979
+ usedDataSources,
980
+ scope: localScope
981
+ });
982
+ const statusExpression = generateExpression({
983
+ expression: page.meta.status ?? "undefined",
984
+ dataSources,
985
+ usedDataSources,
986
+ scope: localScope
987
+ });
988
+ const redirectExpression = generateExpression({
989
+ expression: page.meta.redirect ?? "undefined",
990
+ dataSources,
991
+ usedDataSources,
992
+ scope: localScope
993
+ });
994
+ let customExpression = "";
995
+ customExpression += `[
996
+ `;
997
+ for (const customMeta of page.meta.custom ?? []) {
998
+ if (customMeta.property.trim().length === 0) {
999
+ continue;
1000
+ }
1001
+ const propertyExpression = JSON.stringify(customMeta.property);
1002
+ const contentExpression = generateExpression({
1003
+ expression: customMeta.content,
1004
+ dataSources,
1005
+ usedDataSources,
1006
+ scope: localScope
1007
+ });
1008
+ customExpression += ` {
1009
+ `;
1010
+ customExpression += ` property: ${propertyExpression},
1011
+ `;
1012
+ customExpression += ` content: ${contentExpression},
1013
+ `;
1014
+ customExpression += ` },
1015
+ `;
1016
+ }
1017
+ customExpression += ` ]`;
1018
+ let generated = "";
1019
+ generated += `export const getPageMeta = ({
1020
+ `;
1021
+ generated += ` system,
1022
+ `;
1023
+ generated += ` resources,
1024
+ `;
1025
+ generated += `}: {
1026
+ `;
1027
+ generated += ` system: System;
1028
+ `;
1029
+ generated += ` resources: Record<string, any>;
1030
+ `;
1031
+ generated += `}): PageMeta => {
1032
+ `;
1033
+ for (const dataSource of usedDataSources.values()) {
1034
+ if (dataSource.type === "variable") {
1035
+ const valueName = localScope.getName(dataSource.id, dataSource.name);
1036
+ const initialValueString = JSON.stringify(dataSource.value.value);
1037
+ generated += ` let ${valueName} = ${initialValueString}
1038
+ `;
1039
+ continue;
1040
+ }
1041
+ if (dataSource.type === "parameter") {
1042
+ if (dataSource.id === page.systemDataSourceId) {
1043
+ const valueName = localScope.getName(dataSource.id, dataSource.name);
1044
+ generated += ` let ${valueName} = system
1045
+ `;
1046
+ }
1047
+ continue;
1048
+ }
1049
+ if (dataSource.type === "resource") {
1050
+ const valueName = localScope.getName(dataSource.id, dataSource.name);
1051
+ const resourceName = globalScope.getName(
1052
+ dataSource.resourceId,
1053
+ dataSource.name
1054
+ );
1055
+ generated += ` let ${valueName} = resources.${resourceName}
1056
+ `;
1057
+ continue;
1058
+ }
1059
+ }
1060
+ generated += ` return {
1061
+ `;
1062
+ generated += ` title: ${titleExpression},
1063
+ `;
1064
+ generated += ` description: ${descriptionExpression},
1065
+ `;
1066
+ generated += ` excludePageFromSearch: ${excludePageFromSearchExpression},
1067
+ `;
1068
+ generated += ` language: ${languageExpression},
1069
+ `;
1070
+ generated += ` socialImageAssetId: ${socialImageAssetIdExpression},
1071
+ `;
1072
+ generated += ` socialImageUrl: ${socialImageUrlExpression},
1073
+ `;
1074
+ generated += ` status: ${statusExpression},
1075
+ `;
1076
+ generated += ` redirect: ${redirectExpression},
1077
+ `;
1078
+ generated += ` custom: ${customExpression},
1079
+ `;
1080
+ generated += ` };
1081
+ `;
1082
+ generated += `};
1083
+ `;
1084
+ return generated;
1085
+ };
582
1086
  export {
583
1087
  Asset,
584
1088
  Assets,
@@ -621,14 +1125,25 @@ export {
621
1125
  TextChild,
622
1126
  WebstudioFragment,
623
1127
  createScope,
1128
+ decodeDataSourceVariable,
1129
+ encodeDataSourceVariable,
1130
+ executeExpression,
624
1131
  findPageByIdOrPath,
625
1132
  findParentFolderByChildId,
626
1133
  findTreeInstanceIds,
627
1134
  findTreeInstanceIdsExcludingSlotDescendants,
1135
+ generateExpression,
1136
+ generateFormsProperties,
1137
+ generatePageMeta,
1138
+ generateResourcesLoader,
1139
+ getExpressionIdentifiers,
628
1140
  getPagePath,
629
1141
  getStyleDeclKey,
630
1142
  initialBreakpoints,
1143
+ isLiteralExpression,
631
1144
  isRoot,
1145
+ lintExpression,
632
1146
  loadResource,
633
- parseComponentName
1147
+ parseComponentName,
1148
+ transpileExpression
634
1149
  };
@@ -0,0 +1,107 @@
1
+ import { type Identifier } from "acorn";
2
+ import type { DataSources } from "./schema/data-sources";
3
+ import type { Scope } from "./scope";
4
+ export type Diagnostic = {
5
+ from: number;
6
+ to: number;
7
+ severity: "error" | "hint" | "info" | "warning";
8
+ message: string;
9
+ };
10
+ export declare const lintExpression: ({ expression, availableVariables, allowAssignment, }: {
11
+ expression: string;
12
+ availableVariables?: Set<string> | undefined;
13
+ allowAssignment?: boolean | undefined;
14
+ }) => Diagnostic[];
15
+ /**
16
+ * check whether provided expression is a literal value
17
+ * like "", 0 or { param: "value" }
18
+ * which does not depends on any variable
19
+ */
20
+ export declare const isLiteralExpression: (expression: string) => boolean;
21
+ export declare const getExpressionIdentifiers: (expression: string) => Set<string>;
22
+ /**
23
+ * transpile expression into executable one
24
+ *
25
+ * add optional chaining operator to every member expression
26
+ * to access any field without runtime errors
27
+ *
28
+ * replace variable names if necessary
29
+ */
30
+ export declare const transpileExpression: ({ expression, executable, replaceVariable, }: {
31
+ expression: string;
32
+ executable?: boolean | undefined;
33
+ replaceVariable?: ((identifier: string, assignee: boolean) => string | undefined | void) | undefined;
34
+ }) => string;
35
+ export declare const encodeDataSourceVariable: (id: string) => string;
36
+ export declare const decodeDataSourceVariable: (name: string) => string | undefined;
37
+ export declare const generateExpression: ({ expression, dataSources, usedDataSources, scope, }: {
38
+ expression: string;
39
+ dataSources: Map<string, {
40
+ value: {
41
+ value: number;
42
+ type: "number";
43
+ } | {
44
+ value: string;
45
+ type: "string";
46
+ } | {
47
+ value: boolean;
48
+ type: "boolean";
49
+ } | {
50
+ value: string[];
51
+ type: "string[]";
52
+ } | {
53
+ type: "json";
54
+ value?: unknown;
55
+ };
56
+ type: "variable";
57
+ id: string;
58
+ name: string;
59
+ scopeInstanceId?: string | undefined;
60
+ } | {
61
+ type: "parameter";
62
+ id: string;
63
+ name: string;
64
+ scopeInstanceId?: string | undefined;
65
+ } | {
66
+ type: "resource";
67
+ id: string;
68
+ name: string;
69
+ resourceId: string;
70
+ scopeInstanceId?: string | undefined;
71
+ }>;
72
+ usedDataSources: Map<string, {
73
+ value: {
74
+ value: number;
75
+ type: "number";
76
+ } | {
77
+ value: string;
78
+ type: "string";
79
+ } | {
80
+ value: boolean;
81
+ type: "boolean";
82
+ } | {
83
+ value: string[];
84
+ type: "string[]";
85
+ } | {
86
+ type: "json";
87
+ value?: unknown;
88
+ };
89
+ type: "variable";
90
+ id: string;
91
+ name: string;
92
+ scopeInstanceId?: string | undefined;
93
+ } | {
94
+ type: "parameter";
95
+ id: string;
96
+ name: string;
97
+ scopeInstanceId?: string | undefined;
98
+ } | {
99
+ type: "resource";
100
+ id: string;
101
+ name: string;
102
+ resourceId: string;
103
+ scopeInstanceId?: string | undefined;
104
+ }>;
105
+ scope: Scope;
106
+ }) => string;
107
+ export declare const executeExpression: (expression: undefined | string) => any;