@kernlang/core 3.5.8-canary.210.1.239de0e0 → 3.5.8
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/dist/codegen/body-ts.js +141 -0
- package/dist/codegen/body-ts.js.map +1 -1
- package/dist/codegen/ground-layer.d.ts +8 -0
- package/dist/codegen/ground-layer.js +185 -0
- package/dist/codegen/ground-layer.js.map +1 -1
- package/dist/codegen/portable-logic-primitives.d.ts +197 -1
- package/dist/codegen/portable-logic-primitives.js +144 -0
- package/dist/codegen/portable-logic-primitives.js.map +1 -1
- package/dist/codegen-core.d.ts +1 -1
- package/dist/codegen-core.js +15 -2
- package/dist/codegen-core.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/node-props.d.ts +51 -0
- package/dist/node-props.js.map +1 -1
- package/dist/parser-core.js +10 -0
- package/dist/parser-core.js.map +1 -1
- package/dist/portable-expression-list.d.ts +1 -0
- package/dist/portable-expression-list.js +47 -0
- package/dist/portable-expression-list.js.map +1 -0
- package/dist/portable-predicate.d.ts +18 -0
- package/dist/portable-predicate.js +352 -0
- package/dist/portable-predicate.js.map +1 -0
- package/dist/portable-route-collection.d.ts +3 -0
- package/dist/portable-route-collection.js +22 -0
- package/dist/portable-route-collection.js.map +1 -0
- package/dist/schema.js +238 -9
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +2 -2
- package/dist/spec.js +10 -2
- package/dist/spec.js.map +1 -1
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { isPostfixMutationOperator, isSupportedAssignOperator, SUPPORTED_ASSIGN_OPERATORS, } from './assignment-operators.js';
|
|
19
19
|
import { VALID_TARGETS } from './config.js';
|
|
20
20
|
import { validateCapabilityMetadata, validateImportMetadata } from './import-metadata.js';
|
|
21
|
+
import { parsePortablePredicateProp, validatePortablePredicateAST } from './portable-predicate.js';
|
|
21
22
|
import { defaultRuntime } from './runtime.js';
|
|
22
23
|
import { KERN_VERSION, NODE_TYPES, STYLE_SHORTHANDS, VALUE_SHORTHANDS } from './spec.js';
|
|
23
24
|
// ── Schema Definitions ──────────────────────────────────────────────────
|
|
@@ -588,7 +589,12 @@ export const NODE_SCHEMAS = {
|
|
|
588
589
|
'do',
|
|
589
590
|
'fmt',
|
|
590
591
|
'clamp',
|
|
592
|
+
'firstTruthy',
|
|
593
|
+
'coalesce',
|
|
594
|
+
'firstDefined',
|
|
591
595
|
'objectMerge',
|
|
596
|
+
'objectOmit',
|
|
597
|
+
'objectPick',
|
|
592
598
|
'return',
|
|
593
599
|
'if',
|
|
594
600
|
'else',
|
|
@@ -628,7 +634,12 @@ export const NODE_SCHEMAS = {
|
|
|
628
634
|
'do',
|
|
629
635
|
'fmt',
|
|
630
636
|
'clamp',
|
|
637
|
+
'firstTruthy',
|
|
638
|
+
'coalesce',
|
|
639
|
+
'firstDefined',
|
|
631
640
|
'objectMerge',
|
|
641
|
+
'objectOmit',
|
|
642
|
+
'objectPick',
|
|
632
643
|
'return',
|
|
633
644
|
'if',
|
|
634
645
|
'else',
|
|
@@ -654,7 +665,12 @@ export const NODE_SCHEMAS = {
|
|
|
654
665
|
'do',
|
|
655
666
|
'fmt',
|
|
656
667
|
'clamp',
|
|
668
|
+
'firstTruthy',
|
|
669
|
+
'coalesce',
|
|
670
|
+
'firstDefined',
|
|
657
671
|
'objectMerge',
|
|
672
|
+
'objectOmit',
|
|
673
|
+
'objectPick',
|
|
658
674
|
'return',
|
|
659
675
|
'if',
|
|
660
676
|
'else',
|
|
@@ -669,13 +685,14 @@ export const NODE_SCHEMAS = {
|
|
|
669
685
|
],
|
|
670
686
|
},
|
|
671
687
|
filter: {
|
|
672
|
-
description: 'Declarative `.filter` binding — `filter name=active in=items where="item.active"` lowers to `const active = items.filter(item => item.active);`. Use `item=x` to rename the per-item binding.',
|
|
673
|
-
example: 'filter name=active in=items
|
|
688
|
+
description: 'Declarative `.filter` binding — `filter name=active in=items where="item.active"` lowers to `const active = items.filter(item => item.active);`. Portable routes may use `predicate={{ {eq: ["active", true]} }}` for target-normalized logic. Use `item=x` to rename the per-item binding.',
|
|
689
|
+
example: 'filter name=active in=items predicate={{ {eq: ["active", true]} }}',
|
|
674
690
|
props: {
|
|
675
691
|
name: { required: true, kind: 'identifier' },
|
|
676
692
|
in: { required: true, kind: 'rawExpr' },
|
|
677
693
|
item: { kind: 'identifier' },
|
|
678
|
-
where: {
|
|
694
|
+
where: { kind: 'rawExpr' },
|
|
695
|
+
predicate: { kind: 'rawExpr' },
|
|
679
696
|
type: { kind: 'typeAnnotation' },
|
|
680
697
|
export: { kind: 'boolean' },
|
|
681
698
|
},
|
|
@@ -813,6 +830,39 @@ export const NODE_SCHEMAS = {
|
|
|
813
830
|
export: { kind: 'boolean' },
|
|
814
831
|
},
|
|
815
832
|
},
|
|
833
|
+
firstTruthy: {
|
|
834
|
+
description: 'Declarative ordered truthy fallback binding — `firstTruthy name=label values="preferred, nickname, \'Anonymous\'"` lowers to the first truthy value using TS `||` and Python `or`. Empty string, zero, false, null/None, and undefined all fall through.',
|
|
835
|
+
example: 'firstTruthy name=label values="preferred, nickname, \'Anonymous\'"',
|
|
836
|
+
props: {
|
|
837
|
+
name: { required: true, kind: 'identifier' },
|
|
838
|
+
values: { required: true, kind: 'rawExpr' },
|
|
839
|
+
type: { kind: 'typeAnnotation' },
|
|
840
|
+
trailingComment: { kind: 'string' },
|
|
841
|
+
export: { kind: 'boolean' },
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
coalesce: {
|
|
845
|
+
description: 'Declarative ordered nullish fallback binding — `coalesce name=label values="preferred, nickname, \'Anonymous\'"` lowers to the first defined value using TS `??` and Python None-only fallback logic. Preserves falsy values like empty string, zero, and false.',
|
|
846
|
+
example: 'coalesce name=label values="preferred, nickname, \'Anonymous\'"',
|
|
847
|
+
props: {
|
|
848
|
+
name: { required: true, kind: 'identifier' },
|
|
849
|
+
values: { required: true, kind: 'rawExpr' },
|
|
850
|
+
type: { kind: 'typeAnnotation' },
|
|
851
|
+
trailingComment: { kind: 'string' },
|
|
852
|
+
export: { kind: 'boolean' },
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
firstDefined: {
|
|
856
|
+
description: 'Alias for coalesce — declarative ordered nullish fallback binding.',
|
|
857
|
+
example: 'firstDefined name=label values="preferred, nickname, \'Anonymous\'"',
|
|
858
|
+
props: {
|
|
859
|
+
name: { required: true, kind: 'identifier' },
|
|
860
|
+
values: { required: true, kind: 'rawExpr' },
|
|
861
|
+
type: { kind: 'typeAnnotation' },
|
|
862
|
+
trailingComment: { kind: 'string' },
|
|
863
|
+
export: { kind: 'boolean' },
|
|
864
|
+
},
|
|
865
|
+
},
|
|
816
866
|
objectMerge: {
|
|
817
867
|
description: 'Declarative shallow record merge binding — `objectMerge name=merged sources="base, overrides, { extra: 1 }"` lowers to a new object/dict. Sources are evaluated left-to-right, duplicate keys are last-write-wins, and source records are not mutated.',
|
|
818
868
|
example: 'objectMerge name=merged sources="base, overrides, { extra: 1 }"',
|
|
@@ -824,6 +874,60 @@ export const NODE_SCHEMAS = {
|
|
|
824
874
|
export: { kind: 'boolean' },
|
|
825
875
|
},
|
|
826
876
|
},
|
|
877
|
+
objectPick: {
|
|
878
|
+
description: "Declarative shallow record key selection — `objectPick name=picked in=user keys=\"['id', 'name']\"` lowers to a new object/dict containing the picked keys in order; missing keys are materialized as null/None for cross-target parity.",
|
|
879
|
+
example: "objectPick name=picked in=user keys=\"['id', 'name']\"",
|
|
880
|
+
props: {
|
|
881
|
+
name: { required: true, kind: 'identifier' },
|
|
882
|
+
in: { required: true, kind: 'rawExpr' },
|
|
883
|
+
keys: { required: true, kind: 'rawExpr' },
|
|
884
|
+
type: { kind: 'typeAnnotation' },
|
|
885
|
+
trailingComment: { kind: 'string' },
|
|
886
|
+
export: { kind: 'boolean' },
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
objectOmit: {
|
|
890
|
+
description: 'Declarative shallow record key omission — `objectOmit name=omitted in=user keys="[\'password\']"` lowers to a new object/dict omitting the specified keys.',
|
|
891
|
+
example: 'objectOmit name=omitted in=user keys="[\'password\']"',
|
|
892
|
+
props: {
|
|
893
|
+
name: { required: true, kind: 'identifier' },
|
|
894
|
+
in: { required: true, kind: 'rawExpr' },
|
|
895
|
+
keys: { required: true, kind: 'rawExpr' },
|
|
896
|
+
type: { kind: 'typeAnnotation' },
|
|
897
|
+
trailingComment: { kind: 'string' },
|
|
898
|
+
export: { kind: 'boolean' },
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
objectKeys: {
|
|
902
|
+
description: 'Declarative own-key extraction — `objectKeys name=keys in=user` lowers to Object.keys-style key ordering over a safe record view. Null and primitive inputs produce an empty list in route lowering.',
|
|
903
|
+
example: 'objectKeys name=keys in=user',
|
|
904
|
+
props: {
|
|
905
|
+
name: { required: true, kind: 'identifier' },
|
|
906
|
+
in: { required: true, kind: 'rawExpr' },
|
|
907
|
+
type: { kind: 'typeAnnotation' },
|
|
908
|
+
export: { kind: 'boolean' },
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
objectValues: {
|
|
912
|
+
description: 'Declarative own-value extraction — `objectValues name=values in=user` follows the same key order as objectKeys. Null and primitive inputs produce an empty list in route lowering.',
|
|
913
|
+
example: 'objectValues name=values in=user',
|
|
914
|
+
props: {
|
|
915
|
+
name: { required: true, kind: 'identifier' },
|
|
916
|
+
in: { required: true, kind: 'rawExpr' },
|
|
917
|
+
type: { kind: 'typeAnnotation' },
|
|
918
|
+
export: { kind: 'boolean' },
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
objectEntries: {
|
|
922
|
+
description: 'Declarative own-entry extraction — `objectEntries name=entries in=user` returns two-item [key, value] pairs in Object.entries-style order. Null and primitive inputs produce an empty list in route lowering.',
|
|
923
|
+
example: 'objectEntries name=entries in=user',
|
|
924
|
+
props: {
|
|
925
|
+
name: { required: true, kind: 'identifier' },
|
|
926
|
+
in: { required: true, kind: 'rawExpr' },
|
|
927
|
+
type: { kind: 'typeAnnotation' },
|
|
928
|
+
export: { kind: 'boolean' },
|
|
929
|
+
},
|
|
930
|
+
},
|
|
827
931
|
sort: {
|
|
828
932
|
description: 'Declarative immutable sort — `sort name=sorted in=items compare="a.age - b.age"` lowers to `const sorted = [...items].sort((a, b) => a.age - b.age);`. Source collection is never mutated. Omit `compare` for lexicographic sort. Rename bindings with `a=` / `b=`.',
|
|
829
933
|
example: 'sort name=sorted in=items compare="a.age - b.age"',
|
|
@@ -948,7 +1052,7 @@ export const NODE_SCHEMAS = {
|
|
|
948
1052
|
},
|
|
949
1053
|
},
|
|
950
1054
|
uniqueBy: {
|
|
951
|
-
description: 'Key-based dedup (first-wins, matches Lodash uniqBy) — `uniqueBy name=distinct in=users by="item.id"` emits a Set+filter form.',
|
|
1055
|
+
description: 'Key-based dedup over scalar/hashable selector keys (first-wins, matches Lodash uniqBy) — `uniqueBy name=distinct in=users by="item.id"` emits a Set+filter form.',
|
|
952
1056
|
example: 'uniqueBy name=distinct in=users by="item.id"',
|
|
953
1057
|
props: {
|
|
954
1058
|
name: { required: true, kind: 'identifier' },
|
|
@@ -960,7 +1064,7 @@ export const NODE_SCHEMAS = {
|
|
|
960
1064
|
},
|
|
961
1065
|
},
|
|
962
1066
|
groupBy: {
|
|
963
|
-
description: 'Partition an array into buckets by a key selector. Emits a reduce-based form (compatible with ES2022) — does not depend on `Object.groupBy` (ES2024).',
|
|
1067
|
+
description: 'Partition an array into buckets by a scalar/hashable key selector. Emits a reduce-based form (compatible with ES2022) — does not depend on `Object.groupBy` (ES2024).',
|
|
964
1068
|
example: 'groupBy name=byType in=items by="item.type"',
|
|
965
1069
|
props: {
|
|
966
1070
|
name: { required: true, kind: 'identifier' },
|
|
@@ -972,7 +1076,7 @@ export const NODE_SCHEMAS = {
|
|
|
972
1076
|
},
|
|
973
1077
|
},
|
|
974
1078
|
partition: {
|
|
975
|
-
description: 'Split an array into two by a predicate — single-pass reduce. Emits `const [pass, fail] = ...`. Both `pass` and `fail` prop names are required.',
|
|
1079
|
+
description: 'Split an array into two by a predicate — single-pass reduce. Emits `const [pass, fail] = ...`. Both `pass` and `fail` prop names are required. `type=` denotes the element type for each output list.',
|
|
976
1080
|
example: 'partition pass=active fail=inactive in=users where="item.active"',
|
|
977
1081
|
props: {
|
|
978
1082
|
name: { kind: 'identifier' },
|
|
@@ -986,7 +1090,7 @@ export const NODE_SCHEMAS = {
|
|
|
986
1090
|
},
|
|
987
1091
|
},
|
|
988
1092
|
indexBy: {
|
|
989
|
-
description: 'Array → keyed record via selector. `indexBy name=byId in=users by="item.id"` lowers to `Object.fromEntries(users.map(...))`. Collisions are last-write-wins.',
|
|
1093
|
+
description: 'Array → keyed record via scalar/hashable selector. `indexBy name=byId in=users by="item.id"` lowers to `Object.fromEntries(users.map(...))`. Collisions are last-write-wins.',
|
|
990
1094
|
example: 'indexBy name=byId in=users by="item.id"',
|
|
991
1095
|
props: {
|
|
992
1096
|
name: { required: true, kind: 'identifier' },
|
|
@@ -998,7 +1102,7 @@ export const NODE_SCHEMAS = {
|
|
|
998
1102
|
},
|
|
999
1103
|
},
|
|
1000
1104
|
countBy: {
|
|
1001
|
-
description: 'Count occurrences by key. `countBy name=counts in=items by="item.type"` lowers to a reduce with `Object.create(null)` accumulator (prototype-pollution safe).',
|
|
1105
|
+
description: 'Count occurrences by scalar/hashable selector key. `countBy name=counts in=items by="item.type"` lowers to a reduce with `Object.create(null)` accumulator (prototype-pollution safe).',
|
|
1002
1106
|
example: 'countBy name=counts in=items by="item.type"',
|
|
1003
1107
|
props: {
|
|
1004
1108
|
name: { required: true, kind: 'identifier' },
|
|
@@ -1329,6 +1433,19 @@ export const NODE_SCHEMAS = {
|
|
|
1329
1433
|
export: { kind: 'boolean' },
|
|
1330
1434
|
},
|
|
1331
1435
|
},
|
|
1436
|
+
count: {
|
|
1437
|
+
description: 'Declarative collection cardinality — `count name=total in=items` lowers to collection length; add `where=` to count only matching items without materializing a named filtered collection.',
|
|
1438
|
+
example: 'count name=activeCount in=items where="item.active" type=number',
|
|
1439
|
+
props: {
|
|
1440
|
+
name: { required: true, kind: 'identifier' },
|
|
1441
|
+
in: { required: true, kind: 'rawExpr' },
|
|
1442
|
+
where: { kind: 'rawExpr' },
|
|
1443
|
+
predicate: { kind: 'rawExpr' },
|
|
1444
|
+
item: { kind: 'identifier' },
|
|
1445
|
+
type: { kind: 'typeAnnotation' },
|
|
1446
|
+
export: { kind: 'boolean' },
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1332
1449
|
branch: {
|
|
1333
1450
|
description: 'Pattern-match/switch on an expression — contains `path` children. Top-level form (statement context) emits TS `switch` with `case` blocks. Body-statement form (child of `handler lang="kern"` / `try` / `catch`) emits the same TS `switch` plus a Python `if/elif/else` chain on the fastapi target. Each `path value=X` is a case; `path default=true` is the trailing default case (parallels JS `switch`/`default`). Identifier values like `path value=Status.Active` (unquoted) emit raw refs; quoted strings emit JSON-quoted literals.',
|
|
1334
1451
|
example: 'branch name=route on=path\n path value="/home"\n path value="/about"\n path default=true',
|
|
@@ -1570,7 +1687,12 @@ export const NODE_SCHEMAS = {
|
|
|
1570
1687
|
'do',
|
|
1571
1688
|
'fmt',
|
|
1572
1689
|
'clamp',
|
|
1690
|
+
'firstTruthy',
|
|
1691
|
+
'coalesce',
|
|
1692
|
+
'firstDefined',
|
|
1573
1693
|
'objectMerge',
|
|
1694
|
+
'objectOmit',
|
|
1695
|
+
'objectPick',
|
|
1574
1696
|
'return',
|
|
1575
1697
|
'if',
|
|
1576
1698
|
'else',
|
|
@@ -1656,7 +1778,12 @@ export const NODE_SCHEMAS = {
|
|
|
1656
1778
|
'do',
|
|
1657
1779
|
'fmt',
|
|
1658
1780
|
'clamp',
|
|
1781
|
+
'firstTruthy',
|
|
1782
|
+
'coalesce',
|
|
1783
|
+
'firstDefined',
|
|
1659
1784
|
'objectMerge',
|
|
1785
|
+
'objectOmit',
|
|
1786
|
+
'objectPick',
|
|
1660
1787
|
'return',
|
|
1661
1788
|
'if',
|
|
1662
1789
|
'else',
|
|
@@ -1689,7 +1816,12 @@ export const NODE_SCHEMAS = {
|
|
|
1689
1816
|
'do',
|
|
1690
1817
|
'fmt',
|
|
1691
1818
|
'clamp',
|
|
1819
|
+
'firstTruthy',
|
|
1820
|
+
'coalesce',
|
|
1821
|
+
'firstDefined',
|
|
1692
1822
|
'objectMerge',
|
|
1823
|
+
'objectOmit',
|
|
1824
|
+
'objectPick',
|
|
1693
1825
|
'return',
|
|
1694
1826
|
'if',
|
|
1695
1827
|
'else',
|
|
@@ -1723,7 +1855,12 @@ export const NODE_SCHEMAS = {
|
|
|
1723
1855
|
'do',
|
|
1724
1856
|
'fmt',
|
|
1725
1857
|
'clamp',
|
|
1858
|
+
'firstTruthy',
|
|
1859
|
+
'coalesce',
|
|
1860
|
+
'firstDefined',
|
|
1726
1861
|
'objectMerge',
|
|
1862
|
+
'objectOmit',
|
|
1863
|
+
'objectPick',
|
|
1727
1864
|
'return',
|
|
1728
1865
|
'if',
|
|
1729
1866
|
'else',
|
|
@@ -1792,6 +1929,24 @@ export const NODE_SCHEMAS = {
|
|
|
1792
1929
|
'branch',
|
|
1793
1930
|
'each',
|
|
1794
1931
|
'collect',
|
|
1932
|
+
'filter',
|
|
1933
|
+
'count',
|
|
1934
|
+
'compact',
|
|
1935
|
+
'pluck',
|
|
1936
|
+
'take',
|
|
1937
|
+
'drop',
|
|
1938
|
+
'sort',
|
|
1939
|
+
'objectMerge',
|
|
1940
|
+
'objectOmit',
|
|
1941
|
+
'objectPick',
|
|
1942
|
+
'objectKeys',
|
|
1943
|
+
'objectValues',
|
|
1944
|
+
'objectEntries',
|
|
1945
|
+
'uniqueBy',
|
|
1946
|
+
'groupBy',
|
|
1947
|
+
'partition',
|
|
1948
|
+
'indexBy',
|
|
1949
|
+
'countBy',
|
|
1795
1950
|
'effect',
|
|
1796
1951
|
// Portable side-effect statements — valid as direct route children
|
|
1797
1952
|
// (alongside derive/guard/respond) for mutate-and-persist routes.
|
|
@@ -1893,6 +2048,7 @@ export const NODE_SCHEMAS = {
|
|
|
1893
2048
|
'branch',
|
|
1894
2049
|
'each',
|
|
1895
2050
|
'collect',
|
|
2051
|
+
'count',
|
|
1896
2052
|
'destructure',
|
|
1897
2053
|
'partition',
|
|
1898
2054
|
],
|
|
@@ -2076,7 +2232,7 @@ export const NODE_SCHEMAS = {
|
|
|
2076
2232
|
name: { required: true, kind: 'identifier' },
|
|
2077
2233
|
in: { required: true, kind: 'rawExpr' },
|
|
2078
2234
|
},
|
|
2079
|
-
allowedChildren: ['derive', 'let', 'each', 'fanout', 'emit', 'do', 'assign', 'branch', 'collect'],
|
|
2235
|
+
allowedChildren: ['derive', 'let', 'each', 'fanout', 'emit', 'do', 'assign', 'branch', 'collect', 'count'],
|
|
2080
2236
|
},
|
|
2081
2237
|
stream: {
|
|
2082
2238
|
description: 'Async stream — SSE route (backend), or AsyncGenerator → state with cleanup (Ink). mode=channel for dispatch bridging. Backend SSE bodies may be a raw `handler` (legacy) or a portable body of `derive`/`let`/`each`/`fanout`/`emit`/`do`/`assign`/`branch` nodes (slice 4c) that lowers to the same scaffold without raw JS.',
|
|
@@ -2104,6 +2260,7 @@ export const NODE_SCHEMAS = {
|
|
|
2104
2260
|
'assign',
|
|
2105
2261
|
'branch',
|
|
2106
2262
|
'collect',
|
|
2263
|
+
'count',
|
|
2107
2264
|
],
|
|
2108
2265
|
},
|
|
2109
2266
|
spawn: {
|
|
@@ -3203,6 +3360,60 @@ function checkCrossProps(node, violations, parent) {
|
|
|
3203
3360
|
col: node.loc?.col,
|
|
3204
3361
|
});
|
|
3205
3362
|
}
|
|
3363
|
+
if (node.type === 'filter') {
|
|
3364
|
+
const hasWhere = props.where !== undefined && props.where !== null && String(props.where).trim() !== '';
|
|
3365
|
+
const hasPredicate = props.predicate !== undefined && props.predicate !== null && String(props.predicate).trim() !== '';
|
|
3366
|
+
if (hasPredicate && parent?.type !== 'route' && parent?.type !== 'path') {
|
|
3367
|
+
violations.push({
|
|
3368
|
+
nodeType: 'filter',
|
|
3369
|
+
message: "'filter predicate={{...}}' is supported only as a portable route child",
|
|
3370
|
+
line: node.loc?.line,
|
|
3371
|
+
col: node.loc?.col,
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
if (!hasWhere && !hasPredicate) {
|
|
3375
|
+
violations.push({
|
|
3376
|
+
nodeType: 'filter',
|
|
3377
|
+
message: "'filter' requires either where= or predicate={{...}}",
|
|
3378
|
+
line: node.loc?.line,
|
|
3379
|
+
col: node.loc?.col,
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
if (hasWhere && hasPredicate) {
|
|
3383
|
+
violations.push({
|
|
3384
|
+
nodeType: 'filter',
|
|
3385
|
+
message: "'filter' cannot combine where= and predicate={{...}}",
|
|
3386
|
+
line: node.loc?.line,
|
|
3387
|
+
col: node.loc?.col,
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
if (hasPredicate) {
|
|
3391
|
+
validatePredicateProp(node, violations, props.predicate);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (node.type === 'count') {
|
|
3395
|
+
const hasWhere = props.where !== undefined && props.where !== null && String(props.where).trim() !== '';
|
|
3396
|
+
const hasPredicate = props.predicate !== undefined && props.predicate !== null && String(props.predicate).trim() !== '';
|
|
3397
|
+
if (hasPredicate && parent?.type !== 'route' && parent?.type !== 'path') {
|
|
3398
|
+
violations.push({
|
|
3399
|
+
nodeType: 'count',
|
|
3400
|
+
message: "'count predicate={{...}}' is supported only as a portable route child",
|
|
3401
|
+
line: node.loc?.line,
|
|
3402
|
+
col: node.loc?.col,
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
if (hasWhere && hasPredicate) {
|
|
3406
|
+
violations.push({
|
|
3407
|
+
nodeType: 'count',
|
|
3408
|
+
message: "'count' cannot combine where= and predicate={{...}}",
|
|
3409
|
+
line: node.loc?.line,
|
|
3410
|
+
col: node.loc?.col,
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
if (hasPredicate) {
|
|
3414
|
+
validatePredicateProp(node, violations, props.predicate);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3206
3417
|
if (node.type === 'param') {
|
|
3207
3418
|
// Slice 3c-extension #3: `param` requires `name=` UNLESS it carries
|
|
3208
3419
|
// `binding`/`element` destructure children — destructured params encode
|
|
@@ -3583,6 +3794,24 @@ function checkCrossProps(node, violations, parent) {
|
|
|
3583
3794
|
// both present).
|
|
3584
3795
|
}
|
|
3585
3796
|
}
|
|
3797
|
+
function pushPredicateViolation(node, violations, message) {
|
|
3798
|
+
violations.push({
|
|
3799
|
+
nodeType: node.type,
|
|
3800
|
+
message,
|
|
3801
|
+
line: node.loc?.line,
|
|
3802
|
+
col: node.loc?.col,
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
function validatePredicateProp(node, violations, predicateProp) {
|
|
3806
|
+
const parsed = parsePortablePredicateProp(predicateProp);
|
|
3807
|
+
if (!parsed.ok) {
|
|
3808
|
+
pushPredicateViolation(node, violations, 'predicate must be a valid object literal');
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
for (const message of validatePortablePredicateAST(parsed.value)) {
|
|
3812
|
+
pushPredicateViolation(node, violations, message);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3586
3815
|
function isTruthyProp(raw) {
|
|
3587
3816
|
return raw === true || raw === 'true';
|
|
3588
3817
|
}
|