@shwfed/config 3.0.6 → 3.0.7

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.
Files changed (36) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/config.vue +109 -3
  3. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +68 -68
  4. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +68 -68
  5. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +68 -68
  6. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +68 -68
  7. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +34 -34
  8. package/dist/runtime/components/config/history-picker.vue +6 -4
  9. package/dist/runtime/components/config/prune-dialog.vue +15 -4
  10. package/dist/runtime/components/config/use-editor.js +2 -2
  11. package/dist/runtime/components/form/unit-config.vue +2 -2
  12. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  13. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  14. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  15. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  16. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  17. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  18. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  19. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  20. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  21. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  22. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  23. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  24. package/dist/runtime/components/table/config.vue +2 -2
  25. package/dist/runtime/components/table/schema.d.ts +68 -68
  26. package/dist/runtime/share/clipboard.d.ts +15 -9
  27. package/dist/runtime/share/clipboard.js +22 -10
  28. package/dist/runtime/vendor/cel-js/CLAUDE.md +7 -2
  29. package/dist/runtime/vendor/cel-js/PROMPT.md +27 -0
  30. package/dist/runtime/vendor/cel-js/lib/evaluator.js +2 -0
  31. package/dist/runtime/vendor/cel-js/lib/globals.d.ts +2 -0
  32. package/dist/runtime/vendor/cel-js/lib/globals.js +10 -0
  33. package/dist/runtime/vendor/cel-js/lib/map-builtins.d.ts +2 -0
  34. package/dist/runtime/vendor/cel-js/lib/map-builtins.js +80 -0
  35. package/dist/runtime/vendor/cel-js/lib/operators.js +2 -12
  36. package/package.json +1 -1
@@ -45,10 +45,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
45
45
  readonly data: string;
46
46
  readonly request?: string | undefined;
47
47
  };
48
+ readonly enableSorting?: boolean | undefined;
48
49
  readonly nodeKey: string;
49
50
  readonly nodeChildren: string;
50
51
  readonly nodeLabel: string;
51
- readonly enableSorting?: boolean | undefined;
52
52
  readonly nodeTooltip?: readonly [{
53
53
  readonly locale: "zh";
54
54
  readonly message: string;
@@ -103,10 +103,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
103
103
  readonly data: string;
104
104
  readonly request?: string | undefined;
105
105
  };
106
+ readonly enableSorting?: boolean | undefined;
106
107
  readonly nodeKey: string;
107
108
  readonly nodeChildren: string;
108
109
  readonly nodeLabel: string;
109
- readonly enableSorting?: boolean | undefined;
110
110
  readonly nodeTooltip?: readonly [{
111
111
  readonly locale: "zh";
112
112
  readonly message: string;
@@ -45,10 +45,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
45
45
  readonly data: string;
46
46
  readonly request?: string | undefined;
47
47
  };
48
+ readonly enableSorting?: boolean | undefined;
48
49
  readonly nodeKey: string;
49
50
  readonly nodeChildren: string;
50
51
  readonly nodeLabel: string;
51
- readonly enableSorting?: boolean | undefined;
52
52
  readonly nodeTooltip?: readonly [{
53
53
  readonly locale: "zh";
54
54
  readonly message: string;
@@ -103,10 +103,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
103
103
  readonly data: string;
104
104
  readonly request?: string | undefined;
105
105
  };
106
+ readonly enableSorting?: boolean | undefined;
106
107
  readonly nodeKey: string;
107
108
  readonly nodeChildren: string;
108
109
  readonly nodeLabel: string;
109
- readonly enableSorting?: boolean | undefined;
110
110
  readonly nodeTooltip?: readonly [{
111
111
  readonly locale: "zh";
112
112
  readonly message: string;
@@ -610,14 +610,14 @@ async function copyColumns() {
610
610
  const set = pickedColumnIdSet.value;
611
611
  const cols = orderedColumnIds.value.filter((id) => set.has(id)).map((id) => editingColumns.value.find((c) => c.id === id)).filter((c) => !!c).map(({ groupId: _g, ...rest }) => rest);
612
612
  if (cols.length === 0) return;
613
- const ok = await writeClip("table", cols);
613
+ const ok = await writeClip("table", { items: cols });
614
614
  if (ok) toast.success(`\u5DF2\u590D\u5236 ${cols.length} \u5217`);
615
615
  else toast.error("\u590D\u5236\u5931\u8D25");
616
616
  }
617
617
  async function pasteColumns() {
618
618
  const clip = await readClip();
619
619
  if (!clip || clip.surface !== "table") return;
620
- const { items } = reidFragment(clip.items);
620
+ const { items } = reidFragment(clip);
621
621
  const pasted = items.map(({ groupId: _g, ...rest }) => rest);
622
622
  if (pasted.length === 0) return;
623
623
  editingColumns.value = [...editingColumns.value, ...pasted];
@@ -158,40 +158,6 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
158
158
  readonly message: string;
159
159
  }[]] | undefined;
160
160
  }[];
161
- readonly query?: {
162
- readonly style?: string | undefined;
163
- readonly id?: string | undefined;
164
- readonly initial?: {
165
- readonly data: string;
166
- readonly request?: string | undefined;
167
- } | undefined;
168
- readonly displayName?: string | undefined;
169
- readonly kind: "shwfed.component.form";
170
- readonly fields: readonly any[];
171
- readonly layouts: readonly {
172
- readonly name: string;
173
- readonly layout: {
174
- readonly style?: string | undefined;
175
- readonly columns: number;
176
- readonly gap?: number | undefined;
177
- readonly rows?: number | undefined;
178
- readonly placements: {
179
- readonly [x: string]: {
180
- readonly area: readonly [readonly [number, number], readonly [number, number]];
181
- readonly v?: "stretch" | "center" | "end" | "start" | undefined;
182
- readonly h?: "stretch" | "center" | "end" | "start" | undefined;
183
- };
184
- };
185
- };
186
- readonly media?: string | undefined;
187
- }[];
188
- readonly readonly?: string | undefined;
189
- } | undefined;
190
- readonly dataSource?: {
191
- readonly data: string;
192
- readonly total?: string | undefined;
193
- readonly request?: string | undefined;
194
- } | undefined;
195
161
  readonly actions?: {
196
162
  readonly size: "default" | "sm" | "xs";
197
163
  readonly style?: string | undefined;
@@ -259,6 +225,40 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
259
225
  readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
260
226
  }[];
261
227
  } | undefined;
228
+ readonly query?: {
229
+ readonly style?: string | undefined;
230
+ readonly id?: string | undefined;
231
+ readonly initial?: {
232
+ readonly data: string;
233
+ readonly request?: string | undefined;
234
+ } | undefined;
235
+ readonly displayName?: string | undefined;
236
+ readonly kind: "shwfed.component.form";
237
+ readonly fields: readonly any[];
238
+ readonly layouts: readonly {
239
+ readonly name: string;
240
+ readonly layout: {
241
+ readonly style?: string | undefined;
242
+ readonly columns: number;
243
+ readonly gap?: number | undefined;
244
+ readonly rows?: number | undefined;
245
+ readonly placements: {
246
+ readonly [x: string]: {
247
+ readonly area: readonly [readonly [number, number], readonly [number, number]];
248
+ readonly v?: "stretch" | "center" | "end" | "start" | undefined;
249
+ readonly h?: "stretch" | "center" | "end" | "start" | undefined;
250
+ };
251
+ };
252
+ };
253
+ readonly media?: string | undefined;
254
+ }[];
255
+ readonly readonly?: string | undefined;
256
+ } | undefined;
257
+ readonly dataSource?: {
258
+ readonly data: string;
259
+ readonly total?: string | undefined;
260
+ readonly request?: string | undefined;
261
+ } | undefined;
262
262
  readonly bottomActions?: {
263
263
  readonly size: "default" | "sm" | "xs";
264
264
  readonly style?: string | undefined;
@@ -827,40 +827,6 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
827
827
  readonly message: string;
828
828
  }[]] | undefined;
829
829
  }[];
830
- query?: {
831
- readonly style?: string | undefined;
832
- readonly id?: string | undefined;
833
- readonly initial?: {
834
- readonly data: string;
835
- readonly request?: string | undefined;
836
- } | undefined;
837
- readonly displayName?: string | undefined;
838
- readonly kind: "shwfed.component.form";
839
- readonly fields: readonly any[];
840
- readonly layouts: readonly {
841
- readonly name: string;
842
- readonly layout: {
843
- readonly style?: string | undefined;
844
- readonly columns: number;
845
- readonly gap?: number | undefined;
846
- readonly rows?: number | undefined;
847
- readonly placements: {
848
- readonly [x: string]: {
849
- readonly area: readonly [readonly [number, number], readonly [number, number]];
850
- readonly v?: "stretch" | "center" | "end" | "start" | undefined;
851
- readonly h?: "stretch" | "center" | "end" | "start" | undefined;
852
- };
853
- };
854
- };
855
- readonly media?: string | undefined;
856
- }[];
857
- readonly readonly?: string | undefined;
858
- } | undefined;
859
- dataSource?: {
860
- readonly data: string;
861
- readonly total?: string | undefined;
862
- readonly request?: string | undefined;
863
- } | undefined;
864
830
  actions?: {
865
831
  readonly size: "default" | "sm" | "xs";
866
832
  readonly style?: string | undefined;
@@ -928,6 +894,40 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
928
894
  readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
929
895
  }[];
930
896
  } | undefined;
897
+ query?: {
898
+ readonly style?: string | undefined;
899
+ readonly id?: string | undefined;
900
+ readonly initial?: {
901
+ readonly data: string;
902
+ readonly request?: string | undefined;
903
+ } | undefined;
904
+ readonly displayName?: string | undefined;
905
+ readonly kind: "shwfed.component.form";
906
+ readonly fields: readonly any[];
907
+ readonly layouts: readonly {
908
+ readonly name: string;
909
+ readonly layout: {
910
+ readonly style?: string | undefined;
911
+ readonly columns: number;
912
+ readonly gap?: number | undefined;
913
+ readonly rows?: number | undefined;
914
+ readonly placements: {
915
+ readonly [x: string]: {
916
+ readonly area: readonly [readonly [number, number], readonly [number, number]];
917
+ readonly v?: "stretch" | "center" | "end" | "start" | undefined;
918
+ readonly h?: "stretch" | "center" | "end" | "start" | undefined;
919
+ };
920
+ };
921
+ };
922
+ readonly media?: string | undefined;
923
+ }[];
924
+ readonly readonly?: string | undefined;
925
+ } | undefined;
926
+ dataSource?: {
927
+ readonly data: string;
928
+ readonly total?: string | undefined;
929
+ readonly request?: string | undefined;
930
+ } | undefined;
931
931
  bottomActions?: {
932
932
  readonly size: "default" | "sm" | "xs";
933
933
  readonly style?: string | undefined;
@@ -1,8 +1,11 @@
1
- export type ClipSurface = 'form' | 'block' | 'table';
2
- export type ClipPayload = Readonly<{
3
- surface: ClipSurface;
1
+ export type ClipSurface = 'form' | 'block' | 'table' | 'actions';
2
+ export type ClipFragment = Readonly<{
4
3
  items: ReadonlyArray<unknown>;
5
4
  placements?: Readonly<Record<string, unknown>>;
5
+ groups?: ReadonlyArray<unknown>;
6
+ }>;
7
+ export type ClipPayload = ClipFragment & Readonly<{
8
+ surface: ClipSurface;
6
9
  }>;
7
10
  /**
8
11
  * Deep-clone the fragment with every id (and every reference to a copied id)
@@ -11,16 +14,19 @@ export type ClipPayload = Readonly<{
11
14
  */
12
15
  export declare function reidItems<T>(items: ReadonlyArray<T>): T[];
13
16
  /**
14
- * Reid items and their placements through one shared map, so the placement keys
15
- * (which are item ids) and the items' own `id`s land on the same fresh UUIDs.
17
+ * Reid a whole fragment through one shared map, so every id definition and every
18
+ * reference to one lands on the same fresh UUID: the placement keys (which are
19
+ * item ids), an action item's `groupId` and the group's own `id`, and the items'
20
+ * own `id`s all stay wired to each other.
16
21
  */
17
- export declare function reidFragment<T>(items: ReadonlyArray<T>, placements?: Readonly<Record<string, unknown>>): {
18
- items: T[];
22
+ export declare function reidFragment(fragment: ClipFragment): {
23
+ items: unknown[];
19
24
  placements?: Record<string, unknown>;
25
+ groups?: unknown[];
20
26
  };
21
- export declare function serializeClip(surface: ClipSurface, items: ReadonlyArray<unknown>, placements?: Readonly<Record<string, unknown>>): string;
27
+ export declare function serializeClip(surface: ClipSurface, fragment: ClipFragment): string;
22
28
  export declare function parseClip(text: string): ClipPayload | null;
23
29
  /** Write a fragment to the system clipboard. Resolves `false` on any failure. */
24
- export declare function writeClip(surface: ClipSurface, items: ReadonlyArray<unknown>, placements?: Readonly<Record<string, unknown>>): Promise<boolean>;
30
+ export declare function writeClip(surface: ClipSurface, fragment: ClipFragment): Promise<boolean>;
25
31
  /** Read a fragment from the system clipboard, or `null` if absent/unreadable. */
26
32
  export declare function readClip(): Promise<ClipPayload | null>;
@@ -40,34 +40,46 @@ function buildIdMap(items) {
40
40
  export function reidItems(items) {
41
41
  return remap(items, buildIdMap(items));
42
42
  }
43
- export function reidFragment(items, placements) {
44
- const map = buildIdMap(items);
43
+ export function reidFragment(fragment) {
44
+ const map = buildIdMap([...fragment.items, ...fragment.groups ?? []]);
45
45
  return {
46
- items: remap(items, map),
47
- placements: placements ? remap(placements, map) : void 0
46
+ items: remap(fragment.items, map),
47
+ placements: fragment.placements ? remap(fragment.placements, map) : void 0,
48
+ groups: fragment.groups ? remap(fragment.groups, map) : void 0
48
49
  };
49
50
  }
50
- export function serializeClip(surface, items, placements) {
51
- return JSON.stringify({ [CLIP_KEY]: CLIP_VERSION, surface, items, placements });
51
+ export function serializeClip(surface, fragment) {
52
+ return JSON.stringify({
53
+ [CLIP_KEY]: CLIP_VERSION,
54
+ surface,
55
+ items: fragment.items,
56
+ placements: fragment.placements,
57
+ groups: fragment.groups
58
+ });
52
59
  }
53
60
  function isPlainObject(v) {
54
61
  return v !== null && typeof v === "object" && !Array.isArray(v);
55
62
  }
63
+ const SURFACES = ["form", "block", "table", "actions"];
64
+ function isSurface(v) {
65
+ return typeof v === "string" && SURFACES.includes(v);
66
+ }
56
67
  export function parseClip(text) {
57
68
  try {
58
69
  const o = JSON.parse(text);
59
70
  if (!o || o[CLIP_KEY] !== CLIP_VERSION) return null;
60
- if (o.surface !== "form" && o.surface !== "block" && o.surface !== "table") return null;
71
+ if (!isSurface(o.surface)) return null;
61
72
  if (!Array.isArray(o.items)) return null;
62
73
  const placements = isPlainObject(o.placements) ? o.placements : void 0;
63
- return { surface: o.surface, items: o.items, placements };
74
+ const groups = Array.isArray(o.groups) ? o.groups : void 0;
75
+ return { surface: o.surface, items: o.items, placements, groups };
64
76
  } catch {
65
77
  return null;
66
78
  }
67
79
  }
68
- export async function writeClip(surface, items, placements) {
80
+ export async function writeClip(surface, fragment) {
69
81
  try {
70
- await navigator.clipboard.writeText(serializeClip(surface, items, placements));
82
+ await navigator.clipboard.writeText(serializeClip(surface, fragment));
71
83
  return true;
72
84
  } catch {
73
85
  return false;
@@ -33,6 +33,10 @@ The `__proto__` / `constructor` / `prototype` keys are skipped on the top-level
33
33
 
34
34
  A `JSON` built-in namespace has been added (`json-builtins.ts`) — not from upstream. It registers a `JSON` brand class and a same-named constant on `globalRegistry`, so `JSON.parse(string): dyn` and `JSON.stringify(dyn): string` resolve in every `Environment`. The CEL-side surface mirrors the JS globals deliberately. `JSON.parse` is a thin wrapper over `globalThis.JSON.parse` — numbers come back as plain JS `number` (the evaluator coerces via `Decimal.from` at use time). `JSON.stringify` pre-walks the value via `normalizeForJSON` before handing it to `globalThis.JSON.stringify`: `Decimal` → JS number (via `.toNumber()` — loses precision past safe-integer range, deliberate trade-off for idiomatic JSON output), `Option.Some(x)` → `x`, `Option.None` → `null`, `Map` → plain object, `Date`/`TZDate` passes through so its built-in `toJSON` emits ISO 8601. The pre-walk is required because Effect's `Option` defines its own `toJSON()` that runs before any replacer would see it.
35
35
 
36
+ A `Map` built-in namespace has been added (`map-builtins.ts`) — not from upstream. It registers a `Map` brand class and a same-named constant on `globalRegistry`, so `Map.fromList(list<dyn>): map<dyn, dyn>` and `Map.toList(map<dyn, dyn>): list<map<string, dyn>>` resolve in every `Environment`. This is the only way to *build* a map from data: the `map` macro always yields a list, and spread only accepts a map source, so before this there was no fold from list to map (the JSON round-trip `JSON.parse("{" + rows.map(...).join(",") + "}")` was the only workaround, and it flattens `Decimal` and `Date`). The naming follows Haskell's `Data.Map.fromList` / `toList` rather than a bespoke `toMap` binding macro, because the transform half already exists — `Map.fromList(rows.map(r, {"key": r.name, "value": r.qty}))` composes it. Capitalized to stay clear of the `map` type name and the `map` macro, following `JSON`. A pair is a `{key, value}` record (CEL has no tuple type — the same convention as `list.enumerate()`'s `{index, value}`). Both are plain functions, not macros, so there is no comprehension/accumulator surgery: `fromList` validates each entry and funnels the pairs through the shared `safeFromEntries`, which is what makes a number key stringify *identically* to a map literal's (the plain-object write is the same one `#getMapField` reads back). Both fields are required per entry — a half-pair `{"key": "a"}` throws `invalid_map_entry` rather than storing `undefined` (an explicit `null` value stays legal, via `hasOwn`); keys must be string / number / bool, since `fromList` is the one place a *dyn* key can be checked at all, and an object key would otherwise silently stringify to `"[object Object]"`; duplicate keys are last-wins, which falls out of `safeFromEntries` and coincides with `Data.Map.fromList`'s `foldl insert`. `isRecord` must check the prototype (plain object or `Map`), not just `typeof === 'object'` — `Decimal` / `Date` / `File` are all objects, and without it `Map.fromList([1, 2])` would report a missing `key` field instead of a non-record entry. The handlers' non-list / non-map guards are defensive: overload resolution runs on the *runtime* type, so a non-map reaches neither handler even through a `dyn` expression.
37
+
38
+ `safeFromEntries` and the `__proto__` / `constructor` / `prototype` skip set (`FORBIDDEN_KEYS`) live in `globals.ts`, not `operators.ts` — `map-builtins.ts` needs them and `globals.ts` is the leaf module, so importing from `operators.ts` would have created a cycle. Map literals, map spread (which had its own copy of the set, `SPREAD_SKIP_KEYS`) and `Map.fromList` all funnel through them.
39
+
36
40
  Spread syntax (`...expr`) inside list and map literals — not from upstream. A new `ELLIPSIS` token (3-char lookahead in the lexer) and a `spread` AST op let `[1, ...a, 4]` and `{...a, "x": 1, ...b}` desugar inside the parent literal. The spread node's own `check`/`evaluate` are defensive stubs — `parsePrimary` never produces one, so `...` outside a list/map is an "unexpected token" parse error. List `args` shape is unchanged (`IASTNode[]`, with spread elements detected via `op === 'spread'`); map `args` becomes `([IASTNode, IASTNode] | [IASTNode])[]` — a length-1 tuple represents a spread entry, preserving positional ordering for override semantics (later writes win). The fast path is preserved: the `list`/`map` `check` swaps `evaluate` meta to `evaluateSpreadList`/`evaluateSpreadMap` only when a spread child is present; literals without spreads run the original `resolveAstArray`/`safeFromEntries` paths byte-for-byte. Spread map sources may be plain objects, `Map` instances, or registered message types (typed as `map<string, dyn>`); the `__proto__`/`constructor`/`prototype` skip from `safeFromEntries` is applied to spread sources too. Runtime errors fire on non-list/non-map sources (including `null`). `maxListElements` / `maxMapEntries` count a spread as one entry — runtime expansion bypasses those caps (deliberate trade-off). Call-argument spread (`f(...args)`) is **not** supported.
37
41
 
38
42
  ## Architecture
@@ -52,12 +56,13 @@ lib/
52
56
  ├── http-builder.ts — `HttpRequestBuilder` — what an http.* expression returns
53
57
  ├── json-builtins.ts — `JSON` constant + `JSON.parse` / `JSON.stringify` (local addition)
54
58
  ├── form-builtins.ts — `form(map): FormData` global + `FormData` type (local addition)
59
+ ├── map-builtins.ts — `Map` namespace: fromList / toList — list<{key, value}> ⇄ map (local addition)
55
60
  ├── math-builtins.ts — `math` namespace: trunc / floor / ceil / round / abs / min / max (local addition)
56
61
  ├── decimal.ts — Decimal class (arbitrary precision)
57
62
  ├── optional.ts — Optional type support (uses Effect Option)
58
63
  ├── serialize.ts — AST back to CEL string
59
64
  ├── errors.ts — ParseError, TypeError, EvaluationError
60
- ├── globals.ts — Shared utilities
65
+ ├── globals.ts — Shared utilities + `safeFromEntries` / `FORBIDDEN_KEYS`
61
66
  ├── types.ts — Type definitions
62
67
  └── options.ts — Configuration management
63
68
  ```
@@ -85,7 +90,7 @@ Internal-only (not re-exported from `index.js`): `parse`, standalone `evaluate`/
85
90
 
86
91
  Primitives: `string`, `bool`, `number`, `bytes`, `null_type`, `type`
87
92
  Composites: `list<T>`, `map<K, V>`, `optional<T>`, `dyn`, `message`
88
- Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; host-supplied only — no CEL constructor. Reader `URL.query(string): optional<string>` returns the *first* value of a search param — `None` when absent, distinguishing it from a present-but-empty value. Writers return a **copy**, never mutating the receiver (a `URL` is host-injected shared state, unlike a fresh-per-expression `HttpRequestBuilder`): the set/replace `URL.query(string, string | number | optional<string> | optional<number>): URL` collapses repeated entries and on `None` *deletes* the key (the result mirrors the optional's state — deliberately unlike `HttpRequest.query`, whose `None` is a drop/no-op), while `URL.appendQuery(...)` with the same four value overloads appends an entry and no-ops on `None`. Number values render Decimal-exact, like `string(number)`. `string(URL)` is the full `href`.), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
93
+ Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; host-supplied only — no CEL constructor. Reader `URL.query(string): optional<string>` returns the *first* value of a search param — `None` when absent, distinguishing it from a present-but-empty value. Writers return a **copy**, never mutating the receiver (a `URL` is host-injected shared state, unlike a fresh-per-expression `HttpRequestBuilder`): the set/replace `URL.query(string, string | number | optional<string> | optional<number>): URL` collapses repeated entries and on `None` *deletes* the key (the result mirrors the optional's state — deliberately unlike `HttpRequest.query`, whose `None` is a drop/no-op), while `URL.appendQuery(...)` with the same four value overloads appends an entry and no-ops on `None`. Number values render Decimal-exact, like `string(number)`. `string(URL)` is the full `href`.), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`), `Map` (local addition — the `fromList` / `toList` namespace brand, see `map-builtins.ts`; unrelated to the `map<K, V>` composite)
89
94
 
90
95
  ## Function Signature Format
91
96
 
@@ -211,6 +211,33 @@ unwrapped first so the output is plain JSON — numbers (CEL's `Decimal`) become
211
211
  JS numbers, optionals become their value (or `null` when empty), and `Date`
212
212
  values become ISO 8601 strings.
213
213
 
214
+ ### Map
215
+ ```cel
216
+ // Build a map from a list — the only way to do it, since `map(...)` yields a list:
217
+ Map.fromList([{"key": "a", "value": 1}, {"key": "b", "value": 2}]) // {"a": 1, "b": 2}
218
+
219
+ // Usually you transform a list into pairs first:
220
+ rows.map(r, {"key": r.name, "value": r.qty}) // [{key: "a", value: 1}, ...]
221
+ Map.fromList(rows.map(r, {"key": r.name, "value": r.qty})) // {"a": 1, "b": 2}
222
+
223
+ // Rows already shaped {key, value} go in bare:
224
+ Map.fromList(kvRows)
225
+
226
+ // The inverse — explode a map into pairs so the list macros can reach it:
227
+ Map.toList({"a": 1, "b": 2}) // [{key: "a", value: 1}, {key: "b", value: 2}]
228
+ Map.toList(m).filter(e, e.value > 1).map(e, e.key) // keys whose value > 1
229
+ Map.fromList(Map.toList(m).filter(e, e.value != null)) // drop empty entries, back to a map
230
+ ```
231
+ `Map` is a built-in namespace — always available, like `JSON`. Note the capital
232
+ `M`: lowercase `map` is the list macro, and they are unrelated.
233
+
234
+ A pair is a `{key, value}` record, because CEL has no tuple type — the same
235
+ convention `enumerate()` uses. `Map.fromList(list<dyn>)` requires both fields on
236
+ every entry (a `{"key": "a"}` half-pair is an error; `"value": null` is fine).
237
+ Keys must be `string` / `number` / `bool`. Duplicate keys: the last one wins.
238
+ Values keep their type — numbers stay exact, `Date` stays a `Date` — so prefer
239
+ this over a `JSON.parse`/`JSON.stringify` round-trip, which flattens both.
240
+
214
241
  ### FormData
215
242
  ```cel
216
243
  // Build multipart/form-data for an upload:
@@ -5,6 +5,7 @@ import { registerFunctions } from "./functions.js";
5
5
  import { registerHttpBuiltins } from "./http-builtins.js";
6
6
  import { registerJSONBuiltins } from "./json-builtins.js";
7
7
  import { registerFormBuiltins } from "./form-builtins.js";
8
+ import { registerMapBuiltins } from "./map-builtins.js";
8
9
  import { registerMathBuiltins } from "./math-builtins.js";
9
10
  import { registerMacros } from "./macros.js";
10
11
  import { registerOverloads } from "./overloads.js";
@@ -19,6 +20,7 @@ registerMacros(globalRegistry);
19
20
  registerHttpBuiltins(globalRegistry);
20
21
  registerJSONBuiltins(globalRegistry);
21
22
  registerFormBuiltins(globalRegistry);
23
+ registerMapBuiltins(globalRegistry);
22
24
  registerMathBuiltins(globalRegistry);
23
25
  class Environment {
24
26
  #registry;
@@ -23,5 +23,7 @@ export declare const arrayFrom: {
23
23
  <T>(iterable: Iterable<T> | ArrayLike<T>): T[];
24
24
  <T, U>(iterable: Iterable<T> | ArrayLike<T>, mapfn: (v: T, k: number) => U, thisArg?: any): U[];
25
25
  };
26
+ export declare const FORBIDDEN_KEYS: Set<string>;
27
+ export declare function safeFromEntries(entries: [unknown, unknown][]): Record<string, unknown>;
26
28
  export declare function isAsync(fn: Function | undefined | null, fallback?: boolean): boolean;
27
29
  export declare const RESERVED: Set<string>;
@@ -4,6 +4,16 @@ export const objFreeze = Object.freeze;
4
4
  export const objEntries = Object.entries;
5
5
  export const isArray = Array.isArray;
6
6
  export const arrayFrom = Array.from;
7
+ export const FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
8
+ export function safeFromEntries(entries) {
9
+ const obj = {};
10
+ for (let i = 0; i < entries.length; i++) {
11
+ const [k, v] = entries[i];
12
+ if (FORBIDDEN_KEYS.has(k)) continue;
13
+ obj[k] = v;
14
+ }
15
+ return obj;
16
+ }
7
17
  export function isAsync(fn, fallback) {
8
18
  if (fn?.[Symbol.toStringTag] === "AsyncFunction") return true;
9
19
  return typeof fallback === "boolean" ? fallback : true;
@@ -0,0 +1,2 @@
1
+ import type { Registry } from './registry.js.js';
2
+ export declare function registerMapBuiltins(registry: Registry): void;
@@ -0,0 +1,80 @@
1
+ import { evaluationError } from "./errors.js";
2
+ import { Decimal } from "./decimal.js";
3
+ import { hasOwn, isArray, objKeys, safeFromEntries } from "./globals.js";
4
+ class MapNamespace {
5
+ brand = "Map";
6
+ }
7
+ const MAP_INSTANCE = new MapNamespace();
8
+ function isRecord(v) {
9
+ if (v === null || typeof v !== "object" || isArray(v)) return false;
10
+ if (v instanceof Map) return true;
11
+ const proto = Object.getPrototypeOf(v);
12
+ return proto === Object.prototype || proto === null;
13
+ }
14
+ function hasField(entry, name) {
15
+ return entry instanceof Map ? entry.has(name) : hasOwn(entry, name);
16
+ }
17
+ function readField(entry, name) {
18
+ return entry instanceof Map ? entry.get(name) : entry[name];
19
+ }
20
+ function assertKey(key, index) {
21
+ const ok = typeof key === "string" || typeof key === "boolean" || typeof key === "number" || typeof key === "bigint" || key instanceof Decimal;
22
+ if (!ok) {
23
+ throw evaluationError(
24
+ "invalid_map_key",
25
+ `Map.fromList(): entry ${index} has a '${key === null ? "null" : typeof key}' key (expected string / number / bool)`
26
+ );
27
+ }
28
+ return key;
29
+ }
30
+ export function registerMapBuiltins(registry) {
31
+ registry.registerType("Map", MapNamespace);
32
+ registry.registerVariable({
33
+ name: "Map",
34
+ type: "Map",
35
+ value: MAP_INSTANCE,
36
+ description: "\u6620\u5C04\u6784\u9020\u4E0E\u89E3\u6784"
37
+ });
38
+ registry.registerFunctionOverload(
39
+ "Map.fromList(list<dyn>): map<dyn, dyn>",
40
+ (_r, entries) => {
41
+ if (!isArray(entries)) {
42
+ throw evaluationError(
43
+ "invalid_argument",
44
+ `Map.fromList(): expects a list of {key, value} records`
45
+ );
46
+ }
47
+ const pairs = entries.map((entry, i) => {
48
+ if (!isRecord(entry)) {
49
+ throw evaluationError(
50
+ "invalid_map_entry",
51
+ `Map.fromList(): entry ${i} is not a {key, value} record`
52
+ );
53
+ }
54
+ for (const field of ["key", "value"]) {
55
+ if (!hasField(entry, field)) {
56
+ throw evaluationError(
57
+ "invalid_map_entry",
58
+ `Map.fromList(): entry ${i} has no '${field}' field`
59
+ );
60
+ }
61
+ }
62
+ return [assertKey(readField(entry, "key"), i), readField(entry, "value")];
63
+ });
64
+ return safeFromEntries(pairs);
65
+ }
66
+ );
67
+ registry.registerFunctionOverload(
68
+ "Map.toList(map<dyn, dyn>): list<map<string, dyn>>",
69
+ (_r, m) => {
70
+ if (!isRecord(m)) {
71
+ throw evaluationError("invalid_argument", `Map.toList(): expects a map`);
72
+ }
73
+ if (m instanceof Map) {
74
+ return Array.from(m, ([key, value]) => ({ key, value }));
75
+ }
76
+ const obj = m;
77
+ return objKeys(obj).map((key) => ({ key, value: obj[key] }));
78
+ }
79
+ );
80
+ }
@@ -1,7 +1,7 @@
1
1
  import { Effect, Either, Option } from "effect";
2
2
  import { celTypes } from "./registry.js";
3
3
  import { Decimal } from "./decimal.js";
4
- import { objKeys, isArray, hasOwn } from "./globals.js";
4
+ import { objKeys, isArray, hasOwn, FORBIDDEN_KEYS, safeFromEntries } from "./globals.js";
5
5
  import { attachErrorAst } from "./errors.js";
6
6
  const dynType = celTypes.dyn;
7
7
  export class Base {
@@ -101,10 +101,9 @@ function spreadMapEntryTypes(chk, ctx, node) {
101
101
  node
102
102
  );
103
103
  }
104
- const SPREAD_SKIP_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
105
104
  function assignFragmentEntry(obj, k, v) {
106
105
  const key = k;
107
- if (SPREAD_SKIP_KEYS.has(key)) return;
106
+ if (FORBIDDEN_KEYS.has(key)) return;
108
107
  obj[key] = v;
109
108
  }
110
109
  function mergeMapFragments(ev, frags) {
@@ -354,15 +353,6 @@ function resolveAstArray(ev, astArray, ctx) {
354
353
  if (astArray.length === 0) return Effect.succeed([]);
355
354
  return Effect.all(astArray.map((a) => ev.eval(a, ctx)));
356
355
  }
357
- function safeFromEntries(entries) {
358
- const obj = {};
359
- for (let i = 0; i < entries.length; i++) {
360
- const [k, v] = entries[i];
361
- if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
362
- obj[k] = v;
363
- }
364
- return obj;
365
- }
366
356
  function comprehensionElementType(chk, iterable, ctx) {
367
357
  const iterType = chk.check(iterable, ctx);
368
358
  if (iterType.kind === "dyn") return iterType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {