@sveltejs/kit 3.0.0-next.24 → 3.0.0-next.27

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 (71) hide show
  1. package/package.json +16 -10
  2. package/src/constants.js +6 -0
  3. package/src/core/adapt/builder.js +128 -33
  4. package/src/core/adapt/index.js +3 -0
  5. package/src/core/config/index.js +4 -7
  6. package/src/core/env.js +39 -14
  7. package/src/core/generate_manifest/index.js +42 -43
  8. package/src/core/postbuild/analyse.js +4 -4
  9. package/src/core/postbuild/fallback.js +1 -1
  10. package/src/core/postbuild/prerender.js +4 -4
  11. package/src/core/sync/create_manifest_data/index.js +0 -1
  12. package/src/core/sync/write_app_manifest.js +3 -2
  13. package/src/core/sync/write_server.js +3 -17
  14. package/src/core/utils.js +10 -0
  15. package/src/exports/adapter.js +22 -0
  16. package/src/exports/public.d.ts +101 -49
  17. package/src/exports/vite/build/index.js +1173 -0
  18. package/src/exports/vite/build/remote.js +4 -4
  19. package/src/exports/vite/build/service-worker.js +98 -0
  20. package/src/exports/vite/dev/generate_manifest.js +308 -0
  21. package/src/exports/vite/dev/index.js +40 -282
  22. package/src/exports/vite/index.js +156 -1717
  23. package/src/exports/vite/plugins/env-vars.js +53 -34
  24. package/src/exports/vite/plugins/guard.js +217 -0
  25. package/src/exports/vite/plugins/remote.js +237 -0
  26. package/src/exports/vite/preview/index.js +8 -8
  27. package/src/exports/vite/static_analysis/index.js +15 -15
  28. package/src/exports/vite/utils.js +98 -1
  29. package/src/runner.js +4 -3
  30. package/src/runtime/app/paths/server.js +2 -2
  31. package/src/runtime/app/server/index.js +3 -3
  32. package/src/runtime/app/server/public.d.ts +114 -57
  33. package/src/runtime/app/server/remote/query.js +2 -2
  34. package/src/runtime/app/server/remote/requested.js +31 -15
  35. package/src/runtime/app/state/client.svelte.js +3 -1
  36. package/src/runtime/client/client.js +43 -198
  37. package/src/runtime/client/fetcher.js +6 -6
  38. package/src/runtime/client/focus.js +120 -0
  39. package/src/runtime/client/remote-functions/command.svelte.js +20 -9
  40. package/src/runtime/client/remote-functions/form.svelte.js +12 -7
  41. package/src/runtime/client/remote-functions/query/instance.svelte.js +19 -5
  42. package/src/runtime/client/remote-functions/shared.svelte.js +22 -1
  43. package/src/runtime/client/scroll.js +39 -0
  44. package/src/runtime/client/utils.js +28 -0
  45. package/src/runtime/form-utils.js +254 -264
  46. package/src/runtime/server/data/index.js +14 -36
  47. package/src/runtime/server/errors.js +7 -10
  48. package/src/runtime/server/fetch.js +21 -25
  49. package/src/runtime/server/index.js +35 -31
  50. package/src/runtime/server/internal.js +34 -5
  51. package/src/runtime/server/page/actions.js +6 -8
  52. package/src/runtime/server/page/data_serializer.js +6 -10
  53. package/src/runtime/server/page/index.js +18 -28
  54. package/src/runtime/server/page/load_data.js +2 -2
  55. package/src/runtime/server/page/render.js +22 -40
  56. package/src/runtime/server/page/respond_with_error.js +10 -13
  57. package/src/runtime/server/page/server_routing.js +21 -27
  58. package/src/runtime/server/remote-functions.js +136 -136
  59. package/src/runtime/server/respond.js +66 -64
  60. package/src/runtime/server/state.js +2 -0
  61. package/src/runtime/server/utils.js +4 -11
  62. package/src/runtime/shared.js +9 -0
  63. package/src/runtime/utils.js +41 -0
  64. package/src/types/ambient-private.d.ts +1 -2
  65. package/src/types/global-private.d.ts +8 -0
  66. package/src/types/internal.d.ts +56 -17
  67. package/src/utils/filesystem.js +44 -28
  68. package/src/utils/streaming.js +5 -15
  69. package/src/version.js +1 -1
  70. package/types/index.d.ts +235 -93
  71. package/types/index.d.ts.map +9 -2
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { DEV } from 'esm-env';
5
5
  import * as devalue from 'devalue';
6
- import { text_decoder, text_encoder } from './utils.js';
6
+ import { stream_from_iterable, text_decoder, text_encoder } from './utils.js';
7
7
  import { noop } from '../utils/functions.js';
8
8
  import { SvelteKitError } from '@sveltejs/kit/internal';
9
9
 
@@ -26,6 +26,12 @@ export function set_nested_value(object, field, value) {
26
26
  export function parse_form_key(form_id, key) {
27
27
  const suffix = '/' + form_id;
28
28
  let name = key;
29
+ let image_coordinate = '';
30
+
31
+ if (name.startsWith('i:') && (name.endsWith(suffix + '.x') || name.endsWith(suffix + '.y'))) {
32
+ image_coordinate = name[name.length - 1];
33
+ name = name.slice(0, -2);
34
+ }
29
35
 
30
36
  if (!name.endsWith(suffix)) {
31
37
  throw new Error(`Form contained a field that wasn't created with form.fields.as(...): ${name}`);
@@ -42,10 +48,14 @@ export function parse_form_key(form_id, key) {
42
48
  } else if (name.startsWith('b:')) {
43
49
  name = name.slice(2);
44
50
  type = 'boolean';
51
+ } else if (name.startsWith('i:')) {
52
+ name = name.slice(2);
53
+ type = 'number';
45
54
  }
46
55
 
47
56
  const is_array = name.endsWith('[]');
48
57
  if (is_array) name = name.slice(0, -2);
58
+ if (image_coordinate) name += '.' + image_coordinate;
49
59
 
50
60
  return { name, type, is_array };
51
61
  }
@@ -441,25 +451,17 @@ class LazyFile {
441
451
  }
442
452
  stream() {
443
453
  const range = read_range(this.#get_chunk, this.#offset, this.size);
444
- let cursor = 0;
445
- return new ReadableStream({
446
- pull: async (controller) => {
447
- const { value, done } = await range.next();
448
- if (done) {
449
- if (cursor < this.size) {
450
- controller.error('incomplete file data');
451
- } else {
452
- controller.close();
453
- }
454
- return;
454
+ const size = this.size;
455
+ return stream_from_iterable(
456
+ (async function* () {
457
+ let cursor = 0;
458
+ for await (const chunk of range) {
459
+ cursor += chunk.byteLength;
460
+ yield chunk;
455
461
  }
456
- cursor += value.byteLength;
457
- controller.enqueue(value);
458
- if (cursor >= this.size) {
459
- controller.close();
460
- }
461
- }
462
- });
462
+ if (cursor < size) throw new Error('incomplete file data');
463
+ })()
464
+ );
463
465
  }
464
466
  async text() {
465
467
  return text_decoder.decode(await this.arrayBuffer());
@@ -473,7 +475,12 @@ const path_regex = /^[a-zA-Z_$]\w*(\.[a-zA-Z_$]\w*|\[\d+\])*$/;
473
475
  */
474
476
  export function split_path(path) {
475
477
  if (!path_regex.test(path)) {
476
- throw new Error(`Invalid path ${path}`);
478
+ throw new Error(
479
+ `Invalid field name ${path}` +
480
+ (DEV
481
+ ? ': field names are written in JS object notation, so keys that would need quoting are not supported. See https://svelte.dev/docs/kit/remote-functions#form-Fields'
482
+ : '')
483
+ );
477
484
  }
478
485
 
479
486
  return path.split(/\.|\[|\]/).filter(Boolean);
@@ -600,32 +607,51 @@ export function flatten_issues(issues) {
600
607
  * @param {(string | number)[]} path
601
608
  * @returns {any}
602
609
  */
603
-
604
610
  export function deep_get(object, path) {
605
611
  let current = object;
606
612
  for (const key of path) {
607
- if (current == null || typeof current !== 'object') {
608
- return current;
609
- }
613
+ if (current === null || typeof current !== 'object') return undefined;
610
614
  current = current[key];
611
615
  }
612
616
  return current;
613
617
  }
614
618
 
619
+ /** name prefixes that tell the server which type to coerce a submitted string to */
620
+ const type_prefixes = /** @type {Record<string, string | undefined>} */ ({
621
+ number: 'n:',
622
+ boolean: 'b:'
623
+ });
624
+
625
+ /**
626
+ * adds props; a function becomes a getter that is computed each time it is read
627
+ * @param {Record<string, any>} base_props
628
+ * @param {Record<string, unknown>} props
629
+ */
630
+ function add_props(base_props, props) {
631
+ for (const prop in props) {
632
+ const value = props[prop];
633
+ if (typeof value === 'function') {
634
+ Object.defineProperty(base_props, prop, {
635
+ enumerable: true,
636
+ get: /** @type {() => unknown} */ (value)
637
+ });
638
+ } else {
639
+ base_props[prop] = value;
640
+ }
641
+ }
642
+ return base_props;
643
+ }
644
+
615
645
  /**
616
- *
617
- * @param {string} field_type
646
+ * @param {string} type
618
647
  * @param {boolean} is_array
619
648
  * @param {unknown} input_value
620
649
  */
621
- function get_type_prefix(field_type, is_array, input_value) {
622
- if (field_type === 'number' || field_type === 'range') return 'n:';
623
- if (field_type === 'checkbox' && !is_array) return 'b:';
624
- if (field_type === 'hidden' || field_type === 'submit') {
625
- const input_type = typeof input_value;
626
- if (input_type === 'number') return 'n:';
627
- if (input_type === 'boolean') return 'b:';
628
- }
650
+ function get_type_prefix(type, is_array, input_value) {
651
+ if (type === 'number' || type === 'range') return 'n:';
652
+ if (type === 'image') return 'i:';
653
+ if (type === 'checkbox' && !is_array) return 'b:';
654
+ if (type === 'hidden' || type === 'submit') return type_prefixes[typeof input_value] ?? '';
629
655
  return '';
630
656
  }
631
657
 
@@ -661,270 +687,234 @@ function deep_clone(value) {
661
687
  return value;
662
688
  }
663
689
 
664
- /**
665
- * Creates a proxy-based field accessor for form data
666
- * @param {{
690
+ const warned_sites = new Set();
691
+
692
+ // keyed by the stack, so every place that enumerates warns once with its call site
693
+ const warn_no_keys = () => {
694
+ const error = new Error(
695
+ 'The properties of `form.fields` are virtual, so operators like `in` and `Object.keys` are meaningless. If you need the current value of a form field, use `form.fields.x.value()`'
696
+ );
697
+ if (warned_sites.has(error.stack)) return;
698
+ warned_sites.add(error.stack);
699
+ console.warn(error);
700
+ };
701
+
702
+ // fields are created as they are accessed, so there is nothing to enumerate
703
+ /** @type {ProxyHandler<object> | null} */
704
+ const dev_traps = DEV
705
+ ? {
706
+ has(target, prop) {
707
+ if (typeof prop !== 'symbol') warn_no_keys();
708
+ return prop in target;
709
+ },
710
+ ownKeys(target) {
711
+ warn_no_keys();
712
+ return Reflect.ownKeys(target);
713
+ }
714
+ }
715
+ : null;
716
+
717
+ /** @param {InternalRemoteFormIssue} issue */
718
+ const public_issue = (issue) => ({ path: issue.path, message: issue.message });
719
+
720
+ /** @typedef {{
667
721
  * form_id: string,
668
722
  * get: () => Record<string, any>,
669
723
  * set: (path: (string | number)[], value: any) => void,
670
724
  * get_issues: (path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>,
671
725
  * get_touched: () => Record<string, boolean>,
672
726
  * get_dirty: () => Record<string, boolean>
673
- * }} context - Form context, including value accessors and form metadata
674
- * @param {any} target - Function or empty POJO
675
- * @param {(string | number)[]} path - Current access path
676
- * @returns {any} Proxy object with name(), value(), and issues() methods
677
- */
678
- export function create_field_proxy(context, target = {}, path = []) {
679
- const get_value = () => {
680
- const value = deep_get(context.get(), path);
681
- return deep_clone(value);
682
- };
683
-
684
- return new Proxy(target, {
685
- get(target, prop) {
686
- if (typeof prop === 'symbol') return target[prop];
687
-
688
- // Handle array access like jobs[0]
689
- if (/^\d+$/.test(prop)) {
690
- return create_field_proxy(context, {}, [...path, parseInt(prop, 10)]);
691
- }
727
+ * }} FieldContext */
692
728
 
729
+ /**
730
+ * @param {FieldContext} context
731
+ * @param {(string | number)[]} path
732
+ * @param {string} prop
733
+ * @returns {any} a method of the field at `path`, or undefined for a nested field
734
+ */
735
+ function create_field_method(context, path, prop) {
736
+ switch (prop) {
737
+ case 'set':
738
+ return (/** @type {any} */ value) => {
739
+ context.set(path, value);
740
+ return value;
741
+ };
742
+ case 'value':
743
+ return () => deep_clone(deep_get(context.get(), path));
744
+ case 'issues':
745
+ case 'allIssues': {
746
+ const key = build_path_string(path);
747
+ const all = prop === 'allIssues';
748
+
749
+ return () => {
750
+ const issues = context.get_issues(path, all)[key === '' ? '$' : key];
751
+ if (all) return issues?.map(public_issue);
752
+ const own = issues?.filter((issue) => issue.name === key).map(public_issue);
753
+ return own?.length ? own : undefined;
754
+ };
755
+ }
756
+ case 'touched':
757
+ case 'dirty': {
693
758
  const key = build_path_string(path);
694
- const next = [...path, prop];
695
-
696
- if (prop === 'set') {
697
- const set_func = function (/** @type {any} */ newValue) {
698
- context.set(path, newValue);
699
- return newValue;
700
- };
701
- return create_field_proxy(context, set_func, next);
702
- }
703
-
704
- if (prop === 'value') {
705
- return create_field_proxy(context, get_value, next);
706
- }
707
759
 
708
- if (prop === 'issues' || prop === 'allIssues') {
709
- const issues_func = () => {
710
- const all_issues = context.get_issues(path, prop === 'allIssues')[key === '' ? '$' : key];
760
+ return () => {
761
+ const object = prop === 'dirty' ? context.get_dirty() : context.get_touched();
762
+ if (Object.hasOwn(object, key)) return true;
763
+ for (const candidate in object) {
764
+ if (!Object.hasOwn(object, candidate)) continue;
765
+ if (key === '') return true;
766
+ if (!candidate.startsWith(key)) continue;
767
+ const next = candidate[key.length];
768
+ if (next === '.' || next === '[') return true;
769
+ }
770
+ return false;
771
+ };
772
+ }
773
+ case 'as': {
774
+ const key = build_path_string(path);
711
775
 
712
- if (prop === 'allIssues') {
713
- return all_issues?.map((issue) => ({
714
- path: issue.path,
715
- message: issue.message
716
- }));
776
+ /**
777
+ * the field's value, or `fallback` until the field has been edited
778
+ * (without a fallback there is nothing to suppress, so `dirty` is not read)
779
+ * @param {unknown} [fallback]
780
+ */
781
+ const read = (fallback) =>
782
+ deep_get(context.get(), path) ??
783
+ (fallback !== undefined && Object.hasOwn(context.get_dirty(), key) ? undefined : fallback);
784
+
785
+ /**
786
+ * @param {string} type
787
+ * @param {unknown} [input_value]
788
+ * @param {boolean} [checked]
789
+ */
790
+ return (type, input_value, checked) => {
791
+ const is_array =
792
+ type === 'file multiple' ||
793
+ type === 'select multiple' ||
794
+ (type === 'checkbox' && typeof input_value === 'string');
795
+
796
+ const type_prefix = get_type_prefix(type, is_array, input_value);
797
+
798
+ // Base properties for all input types
799
+ /** @type {Record<string, any>} */
800
+ const base_props = {
801
+ name: type_prefix + key + (is_array ? '[]' : '') + '/' + context.form_id,
802
+ get 'aria-invalid'() {
803
+ const issues = context.get_issues();
804
+ return key in issues ? 'true' : undefined;
717
805
  }
718
-
719
- const issues = all_issues
720
- ?.filter((issue) => issue.name === key)
721
- ?.map((issue) => ({
722
- path: issue.path,
723
- message: issue.message
724
- }));
725
-
726
- return issues?.length ? issues : undefined;
727
806
  };
728
807
 
729
- return create_field_proxy(context, issues_func, next);
730
- }
731
-
732
- if (prop === 'touched' || prop === 'dirty') {
733
- const fn = () => {
734
- const object = prop === 'dirty' ? context.get_dirty() : context.get_touched();
735
-
736
- if (key === '') {
737
- return Object.keys(object).length > 0;
738
- }
739
-
740
- if (Object.hasOwn(object, key)) {
741
- return true;
742
- }
743
-
744
- for (const candidate in object) {
745
- if (!Object.hasOwn(object, candidate)) continue;
746
- if (!candidate.startsWith(key)) continue;
808
+ // Add type attribute only for non-text inputs and non-select elements
809
+ if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
810
+ base_props.type = type === 'file multiple' ? 'file' : type;
811
+ }
747
812
 
748
- const next = candidate[key.length];
749
- if (next === '.' || next === '[') {
750
- return true;
813
+ // Handle submit and hidden inputs
814
+ if (type === 'submit' || type === 'hidden') {
815
+ if (DEV) {
816
+ if (input_value === null || input_value === undefined) {
817
+ throw new Error(`\`${type}\` inputs must have a value`);
751
818
  }
752
819
  }
753
820
 
754
- return false;
755
- };
756
-
757
- return create_field_proxy(context, fn, next);
758
- }
759
-
760
- if (prop === 'as') {
761
- /**
762
- * @param {string} type
763
- * @param {unknown} [input_value]
764
- */
765
- const as_func = (type, input_value) => {
766
- const is_array =
767
- type === 'file multiple' ||
768
- type === 'select multiple' ||
769
- (type === 'checkbox' && typeof input_value === 'string');
770
-
771
- const type_prefix = get_type_prefix(type, is_array, input_value);
772
-
773
- // Base properties for all input types
774
- /** @type {Record<string, any>} */
775
- const base_props = {
776
- name: type_prefix + key + (is_array ? '[]' : '') + '/' + context.form_id,
777
- get 'aria-invalid'() {
778
- const issues = context.get_issues();
779
- return key in issues ? 'true' : undefined;
780
- }
781
- };
782
-
783
- // Add type attribute only for non-text inputs and non-select elements
784
- if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
785
- base_props.type = type === 'file multiple' ? 'file' : type;
786
- }
821
+ return add_props(base_props, {
822
+ value: typeof input_value === 'boolean' ? (input_value ? 'on' : 'off') : input_value
823
+ });
824
+ }
787
825
 
788
- // Handle submit and hidden inputs
789
- if (type === 'submit' || type === 'hidden') {
790
- if (DEV) {
791
- if (input_value === null || input_value === undefined) {
792
- throw new Error(`\`${type}\` inputs must have a value`);
793
- }
826
+ // Handle select inputs
827
+ if (type === 'select' || type === 'select multiple') {
828
+ return add_props(base_props, {
829
+ multiple: is_array,
830
+ value: () => {
831
+ const value = read(input_value);
832
+ // copied, so the state array can't be edited through the props
833
+ return Array.isArray(value) ? [...value] : value;
794
834
  }
835
+ });
836
+ }
795
837
 
796
- const value =
797
- typeof input_value === 'boolean' ? (input_value ? 'on' : 'off') : input_value;
838
+ // Handle checkbox inputs
839
+ if (type === 'checkbox' || type === 'radio') {
840
+ // radio and checkbox array inputs take their option as the second argument
841
+ // and whether it is checked as the third, a single checkbox only the latter
842
+ const has_option = type === 'radio' || is_array;
798
843
 
799
- return Object.defineProperties(base_props, {
800
- value: { value, enumerable: true }
801
- });
844
+ if (DEV && has_option && !input_value) {
845
+ throw new Error(
846
+ `${type === 'radio' ? 'Radio' : 'Checkbox array'} inputs must have a value`
847
+ );
802
848
  }
803
849
 
804
- // Handle select inputs
805
- if (type === 'select' || type === 'select multiple') {
806
- return Object.defineProperties(base_props, {
807
- multiple: { value: is_array, enumerable: true },
808
- value: {
809
- enumerable: true,
810
- get() {
811
- return get_value() ?? input_value;
812
- }
813
- }
814
- });
850
+ if (has_option) {
851
+ base_props.value = input_value ?? 'on';
852
+ } else {
853
+ checked = /** @type {boolean | undefined} */ (input_value);
815
854
  }
816
855
 
817
- // Handle checkbox inputs
818
- if (type === 'checkbox' || type === 'radio') {
819
- if (DEV) {
820
- if (type === 'radio' && !input_value) {
821
- throw new Error('Radio inputs must have a value');
822
- }
856
+ return add_props(base_props, {
857
+ defaultChecked: checked,
858
+ checked: () => {
859
+ const value = read();
860
+ if (value == null) return read(checked);
861
+ if (type === 'radio') return value === input_value;
862
+ if (is_array) return /** @type {unknown[]} */ (value).includes(input_value);
863
+ return value;
864
+ }
865
+ });
866
+ }
823
867
 
824
- if (type === 'checkbox' && is_array && !input_value) {
825
- throw new Error('Checkbox array inputs must have a value');
868
+ // Handle file inputs
869
+ if (type === 'file' || type === 'file multiple') {
870
+ return add_props(base_props, {
871
+ multiple: is_array,
872
+ files: () => {
873
+ const value = read();
874
+ const files = value instanceof File ? [value] : value;
875
+ if (!Array.isArray(files) || !files.every((f) => f instanceof File)) return null;
876
+
877
+ // a FileList-like object where DataTransfer does not exist
878
+ if (typeof DataTransfer === 'undefined') {
879
+ return Object.assign({ length: files.length }, files);
826
880
  }
827
- }
828
881
 
829
- if (type === 'checkbox' && !is_array) {
830
- return Object.defineProperties(base_props, {
831
- defaultChecked: {
832
- enumerable: true,
833
- get() {
834
- return input_value;
835
- }
836
- },
837
- checked: {
838
- enumerable: true,
839
- get() {
840
- return get_value() ?? input_value;
841
- }
842
- }
843
- });
882
+ const transfer = new DataTransfer();
883
+ for (const file of files) transfer.items.add(file);
884
+ return transfer.files;
844
885
  }
886
+ });
887
+ }
845
888
 
846
- return Object.defineProperties(base_props, {
847
- value: { value: input_value ?? 'on', enumerable: true },
848
- checked: {
849
- enumerable: true,
850
- get() {
851
- const value = get_value();
852
-
853
- if (type === 'radio') {
854
- return value === input_value;
855
- }
889
+ if (type === 'image') return base_props;
856
890
 
857
- return (value ?? []).includes(input_value);
858
- }
859
- }
860
- });
861
- }
862
-
863
- // Handle file inputs
864
- if (type === 'file' || type === 'file multiple') {
865
- return Object.defineProperties(base_props, {
866
- multiple: { value: is_array, enumerable: true },
867
- files: {
868
- enumerable: true,
869
- get() {
870
- const value = get_value();
871
-
872
- // Convert File/File[] to FileList-like object
873
- if (value instanceof File) {
874
- // In browsers, we can create a proper FileList using DataTransfer
875
- if (typeof DataTransfer !== 'undefined') {
876
- const fileList = new DataTransfer();
877
- fileList.items.add(value);
878
- return fileList.files;
879
- }
880
- // Fallback for environments without DataTransfer
881
- return { 0: value, length: 1 };
882
- }
883
-
884
- if (Array.isArray(value) && value.every((f) => f instanceof File)) {
885
- if (typeof DataTransfer !== 'undefined') {
886
- const fileList = new DataTransfer();
887
- value.forEach((file) => fileList.items.add(file));
888
- return fileList.files;
889
- }
890
- // Fallback for environments without DataTransfer
891
- /** @type {any} */
892
- const fileListLike = { length: value.length };
893
- value.forEach((file, index) => {
894
- fileListLike[index] = file;
895
- });
896
- return fileListLike;
897
- }
898
-
899
- return null;
900
- }
901
- }
902
- });
903
- }
891
+ // Handle all other input types (text, number, etc.)
892
+ return add_props(base_props, {
893
+ defaultValue: input_value,
894
+ value: () => String(read(input_value) ?? '')
895
+ });
896
+ };
897
+ }
898
+ }
899
+ }
904
900
 
905
- // Handle all other input types (text, number, etc.)
906
- return Object.defineProperties(base_props, {
907
- defaultValue: {
908
- enumerable: true,
909
- get() {
910
- return input_value;
911
- }
912
- },
913
- value: {
914
- enumerable: true,
915
- get() {
916
- const value = get_value() ?? input_value;
917
- return value != null ? String(value) : '';
918
- }
919
- }
920
- });
921
- };
901
+ /**
902
+ * Creates a proxy-based field accessor for form data
903
+ * @param {FieldContext} context
904
+ * @param {any} target - Function or empty POJO
905
+ * @param {(string | number)[]} path - Current access path
906
+ * @returns {any} Proxy object with name(), value(), and issues() methods
907
+ */
908
+ export function create_field_proxy(context, target = {}, path = []) {
909
+ return new Proxy(target, {
910
+ ...dev_traps,
911
+ get(target, prop) {
912
+ if (typeof prop === 'symbol') return target[prop];
922
913
 
923
- return create_field_proxy(context, as_func, next);
924
- }
914
+ // array access like jobs[0]
915
+ const next = [...path, /^\d+$/.test(prop) ? parseInt(prop, 10) : prop];
925
916
 
926
- // Handle property access (nested fields)
927
- return create_field_proxy(context, {}, next);
917
+ return create_field_proxy(context, create_field_method(context, path, prop), next);
928
918
  }
929
919
  });
930
920
  }