@shwfed/config 2.2.0 → 2.2.1

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 (23) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.d.vue.ts +2 -0
  3. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue +31 -0
  4. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue.d.ts +2 -0
  5. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/runtime.vue +12 -2
  6. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/schema.d.ts +1 -0
  7. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/schema.js +4 -0
  8. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.d.vue.ts +2 -0
  9. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.vue +31 -0
  10. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.vue.d.ts +2 -0
  11. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/runtime.vue +16 -8
  12. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/schema.d.ts +1 -0
  13. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/schema.js +4 -0
  14. package/dist/runtime/plugins/i18n/index.js +8 -2
  15. package/dist/runtime/vendor/cel-js/CLAUDE.md +3 -1
  16. package/dist/runtime/vendor/cel-js/PROMPT.md +20 -0
  17. package/dist/runtime/vendor/cel-js/lib/http-builder.d.ts +2 -0
  18. package/dist/runtime/vendor/cel-js/lib/http-builder.js +24 -3
  19. package/dist/runtime/vendor/cel-js/lib/operators.js +139 -11
  20. package/dist/runtime/vendor/cel-js/lib/parser.d.ts +2 -1
  21. package/dist/runtime/vendor/cel-js/lib/parser.js +20 -3
  22. package/dist/runtime/vendor/cel-js/lib/serialize.js +5 -1
  23. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shwfed",
3
3
  "configKey": "shwfed",
4
- "version": "2.2.0",
4
+ "version": "2.2.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -43,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
43
43
  } | undefined;
44
44
  readonly precision?: number | undefined;
45
45
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
46
+ readonly valueAsString?: boolean | undefined;
46
47
  }) => any;
47
48
  }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
48
49
  "onUpdate:modelValue"?: ((value: {
@@ -85,6 +86,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
85
86
  } | undefined;
86
87
  readonly precision?: number | undefined;
87
88
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
89
+ readonly valueAsString?: boolean | undefined;
88
90
  }) => any) | undefined;
89
91
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
90
92
  declare const _default: typeof __VLS_export;
@@ -26,6 +26,7 @@ import {
26
26
  SelectTrigger,
27
27
  SelectValue
28
28
  } from "../../../../ui/select";
29
+ import { Switch } from "../../../../ui/switch";
29
30
  import { getStructFieldDescription, getStructFieldTitle } from "../../../schema";
30
31
  import { DEFAULT_FIELD_ORIENTATION, FIELD_ORIENTATION_OPTIONS } from "../../../utils/common";
31
32
  import { schema } from "./schema";
@@ -76,6 +77,14 @@ function onStepChange(v) {
76
77
  value.value = { ...value.value, step: v };
77
78
  }
78
79
  }
80
+ function onValueAsStringChange(next) {
81
+ if (next) {
82
+ value.value = { ...value.value, valueAsString: true };
83
+ } else {
84
+ const { valueAsString: _omit, ...rest } = value.value;
85
+ value.value = rest;
86
+ }
87
+ }
79
88
  </script>
80
89
 
81
90
  <template>
@@ -300,6 +309,28 @@ function onStepChange(v) {
300
309
  </Field>
301
310
  </div>
302
311
 
312
+ <Field orientation="vertical">
313
+ <FieldLabel class="text-xs text-zinc-500">
314
+ <template
315
+ v-if="fieldDescription('valueAsString')"
316
+ #tooltip
317
+ >
318
+ <Markdown
319
+ :source="fieldDescription('valueAsString')"
320
+ block
321
+ class="prose prose-sm prose-zinc"
322
+ />
323
+ </template>
324
+ {{ fieldTitle("valueAsString") }}
325
+ </FieldLabel>
326
+ <div>
327
+ <Switch
328
+ :model-value="value.valueAsString ?? false"
329
+ @update:model-value="onValueAsStringChange"
330
+ />
331
+ </div>
332
+ </Field>
333
+
303
334
  <div class="grid grid-cols-2 gap-3">
304
335
  <Field orientation="vertical">
305
336
  <FieldLabel class="text-xs text-zinc-500">
@@ -43,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
43
43
  } | undefined;
44
44
  readonly precision?: number | undefined;
45
45
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
46
+ readonly valueAsString?: boolean | undefined;
46
47
  }) => any;
47
48
  }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
48
49
  "onUpdate:modelValue"?: ((value: {
@@ -85,6 +86,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
85
86
  } | undefined;
86
87
  readonly precision?: number | undefined;
87
88
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
89
+ readonly valueAsString?: boolean | undefined;
88
90
  }) => any) | undefined;
89
91
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
90
92
  declare const _default: typeof __VLS_export;
@@ -62,8 +62,18 @@ const formatOptions = computed(() => ({
62
62
  }));
63
63
  const { draft, commit } = useFieldValue({
64
64
  binding: () => props.config.binding,
65
- fromState: (raw) => typeof raw === "number" && Number.isFinite(raw) ? raw : void 0,
66
- toState: (next) => typeof next === "number" && Number.isFinite(next) ? next : null
65
+ fromState: (raw) => {
66
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
67
+ if (typeof raw === "string" && raw.length > 0) {
68
+ const n = Number(raw);
69
+ return Number.isFinite(n) ? n : void 0;
70
+ }
71
+ return void 0;
72
+ },
73
+ toState: (next) => {
74
+ if (typeof next !== "number" || !Number.isFinite(next)) return null;
75
+ return props.config.valueAsString ? String(next) : next;
76
+ }
67
77
  });
68
78
  function applyRounding(n, precision, mode) {
69
79
  const factor = 10 ** precision;
@@ -49,6 +49,7 @@ export declare function schema(configure: (env: Environment) => void): Schema.St
49
49
  precision: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
50
50
  roundingMode: Schema.optional<Schema.Literal<["round", "floor", "ceil"]>>;
51
51
  step: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
52
+ valueAsString: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
52
53
  min: Schema.optional<Schema.Schema<string, string, never>>;
53
54
  max: Schema.optional<Schema.Schema<string, string, never>>;
54
55
  id: Schema.refine<string, typeof Schema.String>;
@@ -55,6 +55,10 @@ export function schema(configure) {
55
55
  title: "\u6B65\u957F",
56
56
  description: "\u53D6\u503C\u7684\u6700\u5C0F\u6B65\u957F\uFF1B\u7559\u7A7A\u65F6\u4E0D\u9650\u5236\uFF08\u4EFB\u610F\u7CBE\u5EA6\uFF09"
57
57
  })),
58
+ valueAsString: Schema.optional(Schema.Boolean.annotations({
59
+ title: "\u4EE5\u5B57\u7B26\u4E32\u8BFB\u5199",
60
+ description: "\u5F00\u542F\u540E\u8868\u5355\u72B6\u6001\u4EE5\u5B57\u7B26\u4E32\u5F62\u5F0F\u5B58\u50A8\u8BE5\u503C\uFF08\u9002\u7528\u4E8E\u540E\u7AEF\u4F7F\u7528 `DECIMAL` / `BIGINT` \u7B49\u9700\u8981\u539F\u6837\u56DE\u4F20\u7684\u5B57\u6BB5\uFF09\uFF1B\u5173\u95ED\u65F6\u82E5\u8BFB\u5230\u7684\u662F\u6570\u503C\u5B57\u7B26\u4E32\u4E5F\u4F1A\u88AB\u89E3\u6790\uFF0C\u4F46\u5199\u56DE\u4ECD\u4E3A\u6570\u503C\u3002\u6CE8\u610F\uFF1A\u8F93\u5165\u65F6\u4ECD\u7ECF\u8FC7 JavaScript \u6D6E\u70B9\u6570\uFF0C\u4EC5\u80FD\u5728\u300C\u88C5\u8F7D-\u56DE\u4F20\u300D\u672A\u7F16\u8F91\u573A\u666F\u4E0B\u4FDD\u7559\u7CBE\u5EA6"
61
+ })),
58
62
  min: Schema.optional(CelNumber.annotations({
59
63
  title: "\u6700\u5C0F\u503C",
60
64
  description: "\u5141\u8BB8\u8F93\u5165\u7684\u6700\u5C0F\u503C\u8868\u8FBE\u5F0F\uFF1B\u7559\u7A7A\u65F6\u4E0D\u9650\u5236\u4E0B\u9650"
@@ -51,6 +51,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
51
51
  readonly precision?: number | undefined;
52
52
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
53
53
  readonly rangeSeparatorIcon?: string | undefined;
54
+ readonly valueAsString?: boolean | undefined;
54
55
  }) => any;
55
56
  }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
56
57
  "onUpdate:modelValue"?: ((value: {
@@ -101,6 +102,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
101
102
  readonly precision?: number | undefined;
102
103
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
103
104
  readonly rangeSeparatorIcon?: string | undefined;
105
+ readonly valueAsString?: boolean | undefined;
104
106
  }) => any) | undefined;
105
107
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
106
108
  declare const _default: typeof __VLS_export;
@@ -28,6 +28,7 @@ import {
28
28
  SelectTrigger,
29
29
  SelectValue
30
30
  } from "../../../../ui/select";
31
+ import { Switch } from "../../../../ui/switch";
31
32
  import { getStructFieldDescription, getStructFieldTitle } from "../../../schema";
32
33
  import { DEFAULT_FIELD_ORIENTATION, FIELD_ORIENTATION_OPTIONS } from "../../../utils/common";
33
34
  import { schema } from "./schema";
@@ -112,6 +113,14 @@ function setSeparatorIcon(next) {
112
113
  value.value = { ...value.value, rangeSeparatorIcon: trimmed };
113
114
  }
114
115
  }
116
+ function onValueAsStringChange(next) {
117
+ if (next) {
118
+ value.value = { ...value.value, valueAsString: true };
119
+ } else {
120
+ const { valueAsString: _omit, ...rest } = value.value;
121
+ value.value = rest;
122
+ }
123
+ }
115
124
  </script>
116
125
 
117
126
  <template>
@@ -468,6 +477,28 @@ function setSeparatorIcon(next) {
468
477
  @update:model-value="setSeparatorIcon"
469
478
  />
470
479
  </Field>
480
+
481
+ <Field orientation="vertical">
482
+ <FieldLabel class="text-xs text-zinc-500">
483
+ <template
484
+ v-if="fieldDescription('valueAsString')"
485
+ #tooltip
486
+ >
487
+ <Markdown
488
+ :source="fieldDescription('valueAsString')"
489
+ block
490
+ class="prose prose-sm prose-zinc"
491
+ />
492
+ </template>
493
+ {{ fieldTitle("valueAsString") }}
494
+ </FieldLabel>
495
+ <div>
496
+ <Switch
497
+ :model-value="value.valueAsString ?? false"
498
+ @update:model-value="onValueAsStringChange"
499
+ />
500
+ </div>
501
+ </Field>
471
502
  </div>
472
503
 
473
504
  <div class="grid grid-cols-3 gap-3">
@@ -51,6 +51,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
51
51
  readonly precision?: number | undefined;
52
52
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
53
53
  readonly rangeSeparatorIcon?: string | undefined;
54
+ readonly valueAsString?: boolean | undefined;
54
55
  }) => any;
55
56
  }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
56
57
  "onUpdate:modelValue"?: ((value: {
@@ -101,6 +102,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
101
102
  readonly precision?: number | undefined;
102
103
  readonly roundingMode?: "round" | "floor" | "ceil" | undefined;
103
104
  readonly rangeSeparatorIcon?: string | undefined;
105
+ readonly valueAsString?: boolean | undefined;
104
106
  }) => any) | undefined;
105
107
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
106
108
  declare const _default: typeof __VLS_export;
@@ -63,15 +63,23 @@ const formatOptions = computed(() => ({
63
63
  useGrouping: false,
64
64
  maximumFractionDigits: 20
65
65
  }));
66
+ function asNumber(raw) {
67
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
68
+ if (typeof raw === "string" && raw.length > 0) {
69
+ const n = Number(raw);
70
+ return Number.isFinite(n) ? n : void 0;
71
+ }
72
+ return void 0;
73
+ }
66
74
  function asRange(raw) {
67
75
  if (!Array.isArray(raw) || raw.length !== 2) return void 0;
68
- const [a, b] = raw;
69
- if (typeof a !== "number" || typeof b !== "number") return void 0;
70
- if (!Number.isFinite(a) || !Number.isFinite(b)) return void 0;
76
+ const a = asNumber(raw[0]);
77
+ const b = asNumber(raw[1]);
78
+ if (a == null || b == null) return void 0;
71
79
  return [a, b];
72
80
  }
73
- function asNumber(raw) {
74
- return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
81
+ function shape(n) {
82
+ return props.config.valueAsString ? String(n) : n;
75
83
  }
76
84
  const uncontrolled = ref(void 0);
77
85
  const model = computed({
@@ -91,11 +99,11 @@ const model = computed({
91
99
  return;
92
100
  }
93
101
  if (typeof binding === "string") {
94
- setAt(binding, next ?? null);
102
+ setAt(binding, next ? [shape(next[0]), shape(next[1])] : null);
95
103
  return;
96
104
  }
97
- setAt(binding[0], next?.[0] ?? null);
98
- setAt(binding[1], next?.[1] ?? null);
105
+ setAt(binding[0], next ? shape(next[0]) : null);
106
+ setAt(binding[1], next ? shape(next[1]) : null);
99
107
  }
100
108
  });
101
109
  const localStart = ref(model.value?.[0]);
@@ -56,6 +56,7 @@ export declare function schema(configure: (env: Environment) => void): Schema.St
56
56
  precision: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
57
57
  roundingMode: Schema.optional<Schema.Literal<["round", "floor", "ceil"]>>;
58
58
  step: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
59
+ valueAsString: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
59
60
  min: Schema.optional<Schema.Schema<string, string, never>>;
60
61
  max: Schema.optional<Schema.Schema<string, string, never>>;
61
62
  rangeSeparatorIcon: Schema.optional<Schema.refine<string, typeof Schema.String>>;
@@ -66,6 +66,10 @@ export function schema(configure) {
66
66
  title: "\u6B65\u957F",
67
67
  description: "\u53D6\u503C\u7684\u6700\u5C0F\u6B65\u957F\uFF1B\u7559\u7A7A\u65F6\u4E0D\u9650\u5236\uFF08\u4EFB\u610F\u7CBE\u5EA6\uFF09"
68
68
  })),
69
+ valueAsString: Schema.optional(Schema.Boolean.annotations({
70
+ title: "\u4EE5\u5B57\u7B26\u4E32\u8BFB\u5199",
71
+ description: "\u5F00\u542F\u540E\u8868\u5355\u72B6\u6001\u4EE5\u5B57\u7B26\u4E32\u5F62\u5F0F\u5B58\u50A8\u4E24\u7AEF\u7684\u503C\uFF08\u9002\u7528\u4E8E\u540E\u7AEF\u4F7F\u7528 `DECIMAL` / `BIGINT` \u7B49\u9700\u8981\u539F\u6837\u56DE\u4F20\u7684\u5B57\u6BB5\uFF09\uFF1B\u5173\u95ED\u65F6\u82E5\u8BFB\u5230\u7684\u662F\u6570\u503C\u5B57\u7B26\u4E32\u4E5F\u4F1A\u88AB\u89E3\u6790\uFF0C\u4F46\u5199\u56DE\u4ECD\u4E3A\u6570\u503C\u3002\u6CE8\u610F\uFF1A\u8F93\u5165\u65F6\u4ECD\u7ECF\u8FC7 JavaScript \u6D6E\u70B9\u6570\uFF0C\u4EC5\u80FD\u5728\u300C\u88C5\u8F7D-\u56DE\u4F20\u300D\u672A\u7F16\u8F91\u573A\u666F\u4E0B\u4FDD\u7559\u7CBE\u5EA6"
72
+ })),
69
73
  min: Schema.optional(CelNumber.annotations({
70
74
  title: "\u6700\u5C0F\u503C",
71
75
  description: "\u5141\u8BB8\u8F93\u5165\u7684\u6700\u5C0F\u503C\u8868\u8FBE\u5F0F\uFF1B\u540C\u65F6\u4F5C\u7528\u4E8E\u8D77\u59CB\u4E0E\u7ED3\u675F\u8F93\u5165"
@@ -1,14 +1,20 @@
1
1
  import { defineNuxtPlugin } from "#app";
2
2
  import { createI18n } from "vue-i18n";
3
+ import { HttpRequestBuilder } from "../../vendor/cel-js/lib/http-builder.js";
3
4
  export default defineNuxtPlugin({
4
5
  name: "shwfed:i18n",
5
6
  setup: (nuxt) => {
6
- nuxt.vueApp.use(createI18n({
7
+ const i18n = createI18n({
7
8
  locale: navigator?.language,
8
9
  legacy: false,
9
10
  fallbackWarn: false,
10
11
  fallbackLocale: "zh",
11
12
  globalInjection: false
12
- }));
13
+ });
14
+ nuxt.vueApp.use(i18n);
15
+ HttpRequestBuilder.setDefaultHeader(
16
+ "Accept-Language",
17
+ () => i18n.global.locale.value
18
+ );
13
19
  }
14
20
  });
@@ -20,7 +20,9 @@ The `homogeneousAggregateLiterals` and `enableOptionalTypes` environment options
20
20
 
21
21
  The custom `Optional` class has been replaced with Effect's `Option` type (`import { Option } from 'effect'`). Internal helpers `optionalOf(value)` and `optionalValue(opt)` in `optional.js` handle the CEL-specific semantics (treating `undefined` as `None`, throwing `EvaluationError` on missing value access).
22
22
 
23
- An `http` built-in has been added (`http-builtins.ts`, `http-builder.ts`) — not from upstream. It registers the `http` constant and the `http` / `HttpRequest` types on `globalRegistry`, so `http.get(url).header(...).body(...)` expressions type-check and evaluate in every `Environment`. A CEL expression only builds an `HttpRequestBuilder` — a pure description of a request; it never issues one. Both terminal methods, `.json()` and `.file()`, are dispatched by the host on the returned builder (neither is a CEL method), so expression evaluation stays free of IO. Endpoints must be absolute URLs — there is no base-URL resolution. Depends on `fx-fetch`.
23
+ An `http` built-in has been added (`http-builtins.ts`, `http-builder.ts`) — not from upstream. It registers the `http` constant and the `http` / `HttpRequest` types on `globalRegistry`, so `http.get(url).header(...).body(...)` expressions type-check and evaluate in every `Environment`. A CEL expression only builds an `HttpRequestBuilder` — a pure description of a request; it never issues one. Both terminal methods, `.json()` and `.file()`, are dispatched by the host on the returned builder (neither is a CEL method), so expression evaluation stays free of IO. Endpoints must be absolute URLs — there is no base-URL resolution. Depends on `fx-fetch`. The host can register process-wide default headers via `HttpRequestBuilder.setDefaultHeader(name, valueOrGetter)` / `clearDefaultHeader(name)`; they are merged in at `#buildRequest()` time, with case-insensitive precedence to an explicit `.header(...)` on the builder. A getter that returns `null` / `undefined` / `''` skips the header for that request, so the host can source values from live refs (e.g. the active i18n locale for `Accept-Language`) without baking in stale snapshots.
24
+
25
+ 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.
24
26
 
25
27
  ## Architecture
26
28
 
@@ -67,6 +67,26 @@ condition ? value_if_true : value_if_false
67
67
  [1, 2] + [3, 4] // [1, 2, 3, 4]
68
68
  ```
69
69
 
70
+ ### Spread (`...expr`)
71
+ A spread inlines another list inside a list literal or another map inside a
72
+ map literal. Order is preserved — a later entry (including a later spread)
73
+ overrides an earlier one when keys collide.
74
+ ```cel
75
+ [1, ...a, 4] // a flattens in position
76
+ [...a, ...b] // equivalent to a + b
77
+
78
+ {...a, "x": 1} // copy a, then set x to 1
79
+ {"x": 1, ...a} // set x, then let a override
80
+ {...a, ...b} // b overrides a on overlapping keys
81
+ {...a, "x": 1, ...b} // a, then x, then b — last write wins
82
+
83
+ [...[1, 2], 3, ...[4]] // [1, 2, 3, 4]
84
+ ```
85
+ Spread is only valid as a list element or map entry — `f(...args)` is not
86
+ supported (CEL resolves overloads by fixed arity). Spreading a non-list into
87
+ a list, or a non-map into a map (including `null`), is an error — at compile
88
+ time when statically known, at runtime when the source is `dyn`.
89
+
70
90
  ### Field access
71
91
  ```cel
72
92
  obj.field // access field on map/object
@@ -13,6 +13,8 @@ import { Effect } from 'effect';
13
13
  import { Fetch } from 'fx-fetch';
14
14
  export declare class HttpRequestBuilder {
15
15
  #private;
16
+ static setDefaultHeader(name: string, value: string | (() => string | null | undefined)): void;
17
+ static clearDefaultHeader(name: string): void;
16
18
  constructor(url: string, method: string);
17
19
  header(name: string, value: string): this;
18
20
  query(key: string, value: string | number): this;
@@ -38,6 +38,20 @@ export class HttpRequestBuilder {
38
38
  #headers = [];
39
39
  #queries = [];
40
40
  #body;
41
+ // Process-wide defaults applied at `#buildRequest()` time. Each entry is
42
+ // keyed by lowercased header name so an explicit `.header('X', …)` on the
43
+ // builder wins regardless of casing. A getter returning `null` / `undefined`
44
+ // / `''` skips the header for that request — lets the host source values
45
+ // from a live ref (e.g. the current i18n locale) without baking in stale
46
+ // snapshots.
47
+ static #defaultHeaders = /* @__PURE__ */ new Map();
48
+ static setDefaultHeader(name, value) {
49
+ const get = typeof value === "function" ? value : () => value;
50
+ HttpRequestBuilder.#defaultHeaders.set(name.toLowerCase(), { name, get });
51
+ }
52
+ static clearDefaultHeader(name) {
53
+ HttpRequestBuilder.#defaultHeaders.delete(name.toLowerCase());
54
+ }
41
55
  constructor(url, method) {
42
56
  this.#url = url;
43
57
  this.#method = method;
@@ -117,9 +131,16 @@ export class HttpRequestBuilder {
117
131
  url: this.#url,
118
132
  method: this.#method
119
133
  };
120
- if (this.#headers.length > 0) {
121
- const headers = {};
122
- for (const [k, v] of this.#headers) headers[k] = v;
134
+ const explicit = new Set(this.#headers.map(([k]) => k.toLowerCase()));
135
+ const headers = {};
136
+ for (const [k, v] of this.#headers) headers[k] = v;
137
+ for (const [key, { name, get }] of HttpRequestBuilder.#defaultHeaders) {
138
+ if (explicit.has(key)) continue;
139
+ const v = get();
140
+ if (v == null || v === "") continue;
141
+ headers[name] = v;
142
+ }
143
+ if (Object.keys(headers).length > 0) {
123
144
  parts.headers = headers;
124
145
  }
125
146
  if (this.#body !== void 0) {
@@ -74,8 +74,106 @@ function checkOptionalAccessNode(chk, ast, ctx) {
74
74
  const actualType = leftType.kind === "optional" ? leftType.valueType : leftType;
75
75
  return chk.registry.getOptionalType(chk.checkAccessOnType(ast, ctx, actualType, true));
76
76
  }
77
- function checkElement(chk, ctx, expected, el) {
78
- return expected.unify(chk.registry, chk.check(el, ctx)) ?? dynType;
77
+ function spreadInner(node) {
78
+ return node.args;
79
+ }
80
+ function spreadListElementType(chk, ctx, node) {
81
+ const t = chk.check(spreadInner(node), ctx);
82
+ if (t.kind === "dyn") return chk.dynType;
83
+ if (t.kind === "list") return t.valueType;
84
+ throw chk.createError(
85
+ "invalid_spread",
86
+ `Cannot spread '${chk.formatType(t)}' into a list (expected a list)`,
87
+ node
88
+ );
89
+ }
90
+ function spreadMapEntryTypes(chk, ctx, node) {
91
+ const t = chk.check(spreadInner(node), ctx);
92
+ if (t.kind === "dyn") return [chk.dynType, chk.dynType];
93
+ if (t.kind === "map") return [t.keyType, t.valueType];
94
+ if (t.kind === "message") {
95
+ const Base2 = chk;
96
+ return [Base2.stringType, chk.dynType];
97
+ }
98
+ throw chk.createError(
99
+ "invalid_spread",
100
+ `Cannot spread '${chk.formatType(t)}' into a map (expected a map)`,
101
+ node
102
+ );
103
+ }
104
+ const SPREAD_SKIP_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
105
+ function assignFragmentEntry(obj, k, v) {
106
+ const key = k;
107
+ if (SPREAD_SKIP_KEYS.has(key)) return;
108
+ obj[key] = v;
109
+ }
110
+ function mergeMapFragments(ev, frags) {
111
+ const obj = {};
112
+ for (const f of frags) {
113
+ if ("kv" in f) {
114
+ assignFragmentEntry(obj, f.kv[0], f.kv[1]);
115
+ continue;
116
+ }
117
+ const src = f.spread;
118
+ if (src instanceof Map) {
119
+ for (const [k, v] of src) assignFragmentEntry(obj, k, v);
120
+ continue;
121
+ }
122
+ if (src !== null && typeof src === "object" && !isArray(src)) {
123
+ for (const k in src) {
124
+ if (hasOwn(src, k)) assignFragmentEntry(obj, k, src[k]);
125
+ }
126
+ continue;
127
+ }
128
+ throw ev.createError(
129
+ "invalid_spread",
130
+ `Cannot spread a non-map value into a map`,
131
+ f.node
132
+ );
133
+ }
134
+ return obj;
135
+ }
136
+ function evaluateSpreadList(ev, ast, ctx) {
137
+ const arr = ast.args;
138
+ return Effect.all(
139
+ arr.map(
140
+ (el) => el.op === "spread" ? ev.eval(spreadInner(el), ctx).pipe(
141
+ Effect.flatMap(
142
+ (v) => isArray(v) ? Effect.succeed(v) : Effect.fail(
143
+ ev.createError(
144
+ "invalid_spread",
145
+ `Cannot spread a non-list value into a list`,
146
+ el
147
+ )
148
+ )
149
+ )
150
+ ) : ev.eval(el, ctx).pipe(Effect.map((v) => [v]))
151
+ )
152
+ ).pipe(Effect.map((parts) => parts.flat()));
153
+ }
154
+ function evaluateSpreadMap(ev, ast, ctx) {
155
+ const arr = ast.args;
156
+ return Effect.all(
157
+ arr.map((e) => {
158
+ if (e.length === 1 || e[0].op === "spread") {
159
+ const node = e[0];
160
+ return ev.eval(spreadInner(node), ctx).pipe(
161
+ Effect.map((src) => ({ spread: src, node }))
162
+ );
163
+ }
164
+ const pair = e;
165
+ return Effect.all([ev.eval(pair[0], ctx), ev.eval(pair[1], ctx)]).pipe(
166
+ Effect.map(([k, v]) => ({ kv: [k, v] }))
167
+ );
168
+ })
169
+ ).pipe(
170
+ Effect.flatMap(
171
+ (frags) => Effect.try({
172
+ try: () => mergeMapFragments(ev, frags),
173
+ catch: (e) => e
174
+ })
175
+ )
176
+ );
79
177
  }
80
178
  function ternaryConditionError(ev, value, node) {
81
179
  const type = ev.debugRuntimeType(value);
@@ -481,9 +579,19 @@ const OPERATORS_MAP = {
481
579
  const arr = ast.args;
482
580
  const arrLen = arr.length;
483
581
  if (arrLen === 0) return ast.setMeta("evaluate", emptyList) && chk.getType("list<T>");
484
- let valueType = chk.check(arr[0], ctx);
485
- for (let i = 1; i < arrLen; i++)
486
- valueType = checkElement(chk, ctx, valueType, arr[i]);
582
+ let hasSpread = false;
583
+ let valueType;
584
+ for (const el of arr) {
585
+ let t;
586
+ if (el.op === "spread") {
587
+ hasSpread = true;
588
+ t = spreadListElementType(chk, ctx, el);
589
+ } else {
590
+ t = chk.check(el, ctx);
591
+ }
592
+ valueType = valueType === void 0 ? t : valueType.unify(chk.registry, t) ?? dynType;
593
+ }
594
+ if (hasSpread) ast.setMeta("evaluate", evaluateSpreadList);
487
595
  return chk.registry.getListType(valueType);
488
596
  },
489
597
  evaluate(ev, ast, ctx) {
@@ -495,13 +603,23 @@ const OPERATORS_MAP = {
495
603
  const arr = ast.args;
496
604
  const arrLen = arr.length;
497
605
  if (arrLen === 0) return ast.setMeta("evaluate", emptyMap) && chk.getType("map<K, V>");
498
- let keyType = chk.check(arr[0][0], ctx);
499
- let valueType = chk.check(arr[0][1], ctx);
500
- for (let i = 1; i < arrLen; i++) {
501
- const e = arr[i];
502
- keyType = checkElement(chk, ctx, keyType, e[0]);
503
- valueType = checkElement(chk, ctx, valueType, e[1]);
606
+ let hasSpread = false;
607
+ let keyType;
608
+ let valueType;
609
+ for (const e of arr) {
610
+ let k;
611
+ let v;
612
+ if (e.length === 1 || e[0].op === "spread") {
613
+ hasSpread = true;
614
+ [k, v] = spreadMapEntryTypes(chk, ctx, e[0]);
615
+ } else {
616
+ k = chk.check(e[0], ctx);
617
+ v = chk.check(e[1], ctx);
618
+ }
619
+ keyType = keyType === void 0 ? k : keyType.unify(chk.registry, k) ?? dynType;
620
+ valueType = valueType === void 0 ? v : valueType.unify(chk.registry, v) ?? dynType;
504
621
  }
622
+ if (hasSpread) ast.setMeta("evaluate", evaluateSpreadMap);
505
623
  return chk.registry.getMapType(keyType, valueType);
506
624
  },
507
625
  evaluate(ev, ast, ctx) {
@@ -513,6 +631,16 @@ const OPERATORS_MAP = {
513
631
  ).pipe(Effect.map(safeFromEntries));
514
632
  }
515
633
  },
634
+ "spread": {
635
+ // Reached only via direct `ast.check`/`ast.evaluate` — parents consume the
636
+ // node inline, so these are defensive guards.
637
+ check(chk, ast) {
638
+ throw chk.createError("misplaced_spread", `'...' is only valid inside a list or map literal`, ast);
639
+ },
640
+ evaluate(ev, ast) {
641
+ return Effect.fail(ev.createError("misplaced_spread", `'...' is only valid inside a list or map literal`, ast));
642
+ }
643
+ },
516
644
  "comprehension": {
517
645
  check(chk, ast, ctx) {
518
646
  const args = ast.args;
@@ -76,8 +76,9 @@ export declare class Parser {
76
76
  parsePostfix(): ASTNode;
77
77
  parsePrimary(): ASTNode;
78
78
  parseList(): ASTNode;
79
+ parseListElement(): ASTNode;
79
80
  parseMap(): ASTNode;
80
- parseProperty(): [ASTNode, ASTNode];
81
+ parseProperty(): [ASTNode, ASTNode] | [ASTNode];
81
82
  parseArgumentList(): ASTNode[];
82
83
  }
83
84
  export {};
@@ -34,7 +34,8 @@ const TOKEN = {
34
34
  COMMA: 28,
35
35
  COLON: 29,
36
36
  QUESTION: 30,
37
- BYTES: 31
37
+ BYTES: 31,
38
+ ELLIPSIS: 32
38
39
  };
39
40
  const OP_FOR_TOKEN = {
40
41
  [TOKEN.EQ]: OPS["=="],
@@ -208,6 +209,8 @@ class Lexer {
208
209
  case "}":
209
210
  return this.token(this.pos++, TOKEN.RBRACE);
210
211
  case ".":
212
+ if (input[pos + 1] === "." && input[pos + 2] === ".")
213
+ return this.token((this.pos += 3) - 3, TOKEN.ELLIPSIS);
211
214
  return this.token(this.pos++, TOKEN.DOT);
212
215
  case ",":
213
216
  return this.token(this.pos++, TOKEN.COMMA);
@@ -735,12 +738,12 @@ export class Parser {
735
738
  const elements = [];
736
739
  let remainingElements = this.limits.maxListElements;
737
740
  if (!this.match(TOKEN.RBRACKET)) {
738
- elements.push(this.parseExpression());
741
+ elements.push(this.parseListElement());
739
742
  if (!remainingElements--) this.#limitExceeded("maxListElements", elements.at(-1).pos);
740
743
  while (this.match(TOKEN.COMMA)) {
741
744
  this.#advanceToken();
742
745
  if (this.match(TOKEN.RBRACKET)) break;
743
- elements.push(this.parseExpression());
746
+ elements.push(this.parseListElement());
744
747
  if (!remainingElements--) this.#limitExceeded("maxListElements", elements.at(-1).pos);
745
748
  }
746
749
  }
@@ -748,6 +751,10 @@ export class Parser {
748
751
  this.consume(TOKEN.RBRACKET);
749
752
  return this.#node(start, closeEnd, OPS.list, elements);
750
753
  }
754
+ parseListElement() {
755
+ if (this.match(TOKEN.ELLIPSIS)) return this.#parseSpread();
756
+ return this.parseExpression();
757
+ }
751
758
  parseMap() {
752
759
  const start = this.consume(TOKEN.LBRACE);
753
760
  const props = [];
@@ -767,8 +774,18 @@ export class Parser {
767
774
  return this.#node(start, closeEnd, OPS.map, props);
768
775
  }
769
776
  parseProperty() {
777
+ if (this.match(TOKEN.ELLIPSIS)) return [this.#parseSpread()];
770
778
  return [this.parseExpression(), (this.consume(TOKEN.COLON), this.parseExpression())];
771
779
  }
780
+ // `...expr` — only valid as a list element or map entry; the parent literal
781
+ // consumes the node and `parsePrimary` never sees ELLIPSIS, so a stray `...`
782
+ // surfaces as a normal "unexpected token" parse error.
783
+ #parseSpread() {
784
+ const start = this.pos;
785
+ this.#advanceToken();
786
+ const inner = this.parseExpression();
787
+ return this.#node(start, inner.end, OPS.spread, inner);
788
+ }
772
789
  parseArgumentList() {
773
790
  const args = [];
774
791
  let remainingArgs = this.limits.maxCallArguments;
@@ -79,7 +79,11 @@ export function serialize(ast) {
79
79
  case "list":
80
80
  return `[${args.map(serialize).join(", ")}]`;
81
81
  case "map":
82
- return `{${args.map(([k, v]) => `${serialize(k)}: ${serialize(v)}`).join(", ")}}`;
82
+ return `{${args.map(
83
+ (e) => e.length === 1 || e[0].op === "spread" ? serialize(e[0]) : `${serialize(e[0])}: ${serialize(e[1])}`
84
+ ).join(", ")}}`;
85
+ case "spread":
86
+ return `...${serialize(args)}`;
83
87
  case "?:": {
84
88
  const ternArgs = args;
85
89
  return `${wrap(ternArgs[0], op)} ? ${wrap(ternArgs[1], op)} : ${serialize(ternArgs[2])}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {