Skip to content

Conversation

@Teyik0
Copy link
Contributor

@Teyik0 Teyik0 commented Nov 22, 2025

This PR auto stringify formdata related to issue #228 when using t.ArrayString and t.ObjectString avoiding manual JSON.stringify.
It also enhance t.File and t.Files support related to this PR in Elysia adding BunFile support to t.File and t.Files.

All test are done and fully working.
Do not merge if the BunFile PR in Elysia related is not merged too.

With this PR Documentation effort should be done to guide users that want to use nested data with files.

Summary by CodeRabbit

  • New Features

    • Enhanced FormData construction with improved support for mixed-type multipart form data, including proper handling of files, arrays, and nested structures.
    • Added compatibility for Bun's File type alongside standard File objects in form submissions.
  • Tests

    • Added comprehensive test coverage for file uploads and nested form data scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

Enhances FormData construction logic to automatically handle File objects, BunFile, and nested arrays, introducing helper functions for stringification decisions and value preparation. Updates type signatures to relax File array constraints using generic helpers, enabling more flexible multipart form data composition. Adds comprehensive test coverage for file handling and nested form data scenarios.

Changes

Cohort / File(s) Summary
FormData Construction Enhancements
src/treaty2/index.ts
Adds shouldStringify() and prepareValue() helper functions to intelligently handle mixed-type arrays, File/BunFile objects, and non-file objects during FormData assembly. Adjusts array handling to stringify entire arrays when they contain non-file objects, otherwise appends elements individually with proper conversions. Updates generic field appending to use the new prepareValue function.
Type System Improvements
src/treaty2/types.ts
Introduces MaybeArrayFile<T> and RelaxFileArrays<T> generic helpers for relaxed File array typing. Updates Treaty.Sign body parameter signatures across all applicable routes to use RelaxFileArrays<Body> instead of Body, enabling broader File/BunFile union type acceptance while maintaining type safety. Adds BunFile import compatibility.
New Test Suites
test/treaty2-files.test.ts, test/treaty2-nested-form.test.ts
Introduces integration tests for File/Files route handling with type-level assertions, testing single Bun.file and File acceptance, array handling, and error cases. Adds tests for nested FormData with files, demonstrating both automatic and manual stringification behavior for structured form data.
Test Cleanup
test/treaty2.test.ts
Removes deprecated file upload/download test cases and old t.File/t.Files test suite. Simplifies import statement to exclude unused Treaty export. Reflowed remaining tests into more concise structure.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant treaty
    participant FormData Handler
    participant prepareValue
    participant Server

    Client->>treaty: send multipart form data<br/>(files + nested objects + arrays)
    treaty->>FormData Handler: buildFormData(body)
    
    FormData Handler->>FormData Handler: iterate body entries
    
    alt Array field
        FormData Handler->>FormData Handler: shouldStringify(array)
        alt contains non-file objects
            FormData Handler->>FormData Handler: stringify entire array
            FormData Handler->>FormData Handler: append as single value
        else all file types or primitives
            loop each array element
                FormData Handler->>prepareValue: prepare(element)
                alt File or BunFile
                    prepareValue->>prepareValue: createNewFile(file)
                    prepareValue-->>FormData Handler: converted File
                else non-file object
                    prepareValue->>prepareValue: JSON.stringify(object)
                    prepareValue-->>FormData Handler: stringified value
                else primitive
                    prepareValue-->>FormData Handler: unchanged value
                end
                FormData Handler->>FormData Handler: append(name, prepared)
            end
        end
    else Non-array field
        FormData Handler->>prepareValue: prepare(value)
        prepareValue-->>FormData Handler: processed value
        FormData Handler->>FormData Handler: append(name, prepared)
    end
    
    FormData Handler-->>treaty: FormData constructed
    treaty->>Server: POST with FormData
    Server-->>Client: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • src/treaty2/index.ts: Logic for array stringification decisions and value preparation requires careful reasoning about edge cases (nested arrays, mixed types, File conversions).
  • src/treaty2/types.ts: Generic type constraints and conditional type distributions need verification for correctness across all routing branches.
  • Test comprehensiveness: Ensure tests adequately cover File, BunFile, mixed arrays, and error conditions to validate the implementation assumptions.

Possibly related issues

Possibly related PRs

  • Fix file upload bug on server #124: Modifies server-side FormData array handling to append elements individually rather than as single strings, aligning with the same FormData construction approach adopted in this PR.

Poem

🐰 Files in arrays, objects all around,
BunFile and File, now type-safe and sound!
FormData flows with prepareValue's grace,
No stringify, just nested data's place. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly describes the main change: auto-stringifying ArrayString/ObjectString fields in FormData construction, which aligns with the primary objective stated in the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Teyik0 Teyik0 changed the title Auto-stringify ArrayString/ObjectString in FormData feat: Auto-stringify ArrayString/ObjectString in FormData Nov 22, 2025
@Teyik0 Teyik0 changed the title feat: Auto-stringify ArrayString/ObjectString in FormData fix: Auto-stringify ArrayString/ObjectString in FormData Nov 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/treaty2/types.ts (1)

2-7: Switch BunFile to a type-only import to avoid runtime dependency on the bun module

The type helpers are correct:

  • MaybeArrayFile<T> widens (File | BunFile)[] fields to accept single File | BunFile
  • RelaxFileArrays<T> applies this to object bodies and is correctly wired into all non-GET/HEAD body positions in Treaty.Sign

The import at line 5 needs correction:

import { BunFile } from 'bun'

This is a value import, which compiles to a runtime require('bun'). Since BunFile is only used in type positions (line 64 in MaybeArrayFile<T>), the runtime dependency will break consumers in environments without Bun (Node, browser bundlers, etc.).

Change to:

-import { BunFile } from 'bun'
+import type { BunFile } from 'bun'

This keeps type compatibility without forcing a runtime dependency.

🧹 Nitpick comments (2)
test/treaty2-nested-form.test.ts (1)

5-21: Type alias naming and deep equality on file field could be made more robust

  • Reusing the identifier productModel for both the schema value and the type alias is valid but hard to read; consider renaming the type to something like ProductModel to follow TS conventions and avoid confusion between value/type spaces.
  • Both tests assert toEqual(testProduct), which includes the image: Bun.file(...) field. This assumes the server returns the exact same BunFile instance; if Elysia ever clones or wraps files, these assertions could become brittle. A safer pattern is to assert structural equality on the data parts and check the file separately (e.g. name, size, type).

Also applies to: 30-48, 68-82

test/treaty2.test.ts (1)

95-187: Clarify why some /empty-test cases are skipped in the test matrix

The /empty-test group defines more variants (with-maybe-empty, with-optional, with-union-undefined) than are actually exercised in the test.each matrix, where those three are commented out. That’s fine, but it would help future readers if there were a short comment/TODO explaining why those specific cases are excluded (e.g. known limitation, pending fix, or intentionally different behavior).

Also applies to: 440-447

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5441e9 and 1a00f94.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • src/treaty2/index.ts (2 hunks)
  • src/treaty2/types.ts (5 hunks)
  • test/treaty2-files.test.ts (1 hunks)
  • test/treaty2-nested-form.test.ts (1 hunks)
  • test/treaty2.test.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/treaty2/types.ts (1)
src/types.ts (1)
  • Prettify (88-90)
test/treaty2-nested-form.test.ts (1)
src/treaty2/index.ts (1)
  • treaty (568-594)
test/treaty2-files.test.ts (1)
src/treaty2/index.ts (1)
  • treaty (568-594)
🔇 Additional comments (1)
test/treaty2-files.test.ts (1)

1-6: BunFile import is actively used in type assertions—review claim is incorrect

The import { BunFile } from 'bun' on line 5 is not unused. It appears in the type assertions on lines 60, 66, and 72:

expectTypeOf<RouteFiles>().parameter(0).toEqualTypeOf<{
    files: Array<File | BunFile> | File | BunFile
}>()

However, one valid issue remains: the third type test on line 69 duplicates line 63. Both assign client['any']['file']['post'] to their respective types. The suggestion to change line 69 to client['png']['file']['post'] is correct and would properly exercise the PNG-specific route.

The async describe pattern, while unconventional compared to beforeAll, is supported by Bun's test runner and works as intended here.

Likely an incorrect or invalid review comment.

Comment on lines +320 to +343
const shouldStringify = (value: any): boolean => {
if (typeof value === 'string') return false
if (isFile(value)) return false

// Objects and Arrays should be stringified
if (typeof value === 'object' && value !== null) {
return true
}

return false
}

const prepareValue = async (value: any): Promise<any> => {
if (value instanceof File) {
return await createNewFile(value)
}

if (shouldStringify(value)) {
return JSON.stringify(value)
}

return value
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Narrow shouldStringify to avoid surprising behavior for non-plain objects (e.g. Date) in multipart bodies

Right now shouldStringify treats any non-null object that isn’t detected as a file as JSON-serializable:

const shouldStringify = (value: any): boolean => {
    if (typeof value === 'string') return false
    if (isFile(value)) return false

    if (typeof value === 'object' && value !== null) {
        return true
    }

    return false
}

This means that in multipart/form-data requests with files:

  • Date instances (and other non-plain objects) will be turned into JSON.stringify(value) (e.g. "\"2025-01-01T...Z\""), which is different from how dates are encoded in the JSON body path ({ date: new Date() }"2025-01-01T...Z" without the extra quoting).
  • Any custom value object that shouldn’t be JSON-encoded as a whole will also be stringified.

A safer approach would be:

  • Only stringify plain objects and arrays.
  • Treat Date specially, mapping to .toISOString().
  • Leave other object types as-is.

For example:

-        const shouldStringify = (value: any): boolean => {
-            if (typeof value === 'string') return false
-            if (isFile(value)) return false
-
-            // Objects and Arrays should be stringified
-            if (typeof value === 'object' && value !== null) {
-                return true
-            }
-
-            return false
-        }
-
-        const prepareValue = async (value: any): Promise<any> => {
-            if (value instanceof File) {
-                return await createNewFile(value)
-            }
-
-            if (shouldStringify(value)) {
-                return JSON.stringify(value)
-            }
-
-            return value
-        }
+        const isPlainObject = (value: any): boolean =>
+            value !== null &&
+            typeof value === 'object' &&
+            !Array.isArray(value) &&
+            !isFile(value) &&
+            !(value instanceof Date)
+
+        const shouldStringify = (value: any): boolean => {
+            if (typeof value === 'string') return false
+            if (isFile(value)) return false
+            if (Array.isArray(value)) return true
+            if (isPlainObject(value)) return true
+            return false
+        }
+
+        const prepareValue = async (value: any): Promise<any> => {
+            if (value instanceof File) {
+                return await createNewFile(value)
+            }
+
+            if (value instanceof Date) {
+                // Match JSON body behavior for t.Date()
+                return value.toISOString()
+            }
+
+            if (shouldStringify(value)) {
+                return JSON.stringify(value)
+            }
+
+            return value
+        }

This keeps the intended behavior for t.ArrayString / t.ObjectString while reducing the chance of surprising encodings for other schema types.

Also applies to: 418-419


🏁 Script executed:

#!/bin/bash
# Examine the code context around lines 320-343 in src/treaty2/index.ts
# and look for related code handling prepareValue, form-data, and Date serialization

# First, check the exact code around the flagged lines
echo "=== Code at lines 320-343 ==="
sed -n '310,350p' src/treaty2/index.ts

echo -e "\n=== Search for Date handling in the file ==="
rg -n 'Date|date' src/treaty2/index.ts -C 3

echo -e "\n=== Search for ArrayString or ObjectString ==="
rg -n 'ArrayString|ObjectString' src/treaty2/index.ts -C 2

echo -e "\n=== Search for prepareValue usage ==="
rg -n 'prepareValue' src/treaty2/index.ts -C 3

echo -e "\n=== Search for test files related to multipart/form-data ==="
fd -e test.ts -e spec.ts -e test.js -e spec.js | head -20

Length of output: 5033


🏁 Script executed:

#!/bin/bash
# Look for test coverage of form-data with objects/arrays
# and how Date is currently being serialized

echo "=== Search for multipart/form-data tests ==="
rg -l 'multipart|form-data|FormData' --type=ts --type=js

echo -e "\n=== Search for Date-related tests ==="
rg -n 'new Date\(\)|Date\(' src/treaty2/ --type=ts -A 2 -B 2 | head -50

Length of output: 540


🏁 Script executed:

#!/bin/bash
# Check parseStringifiedDate implementation and JSON serialization in the JSON body path

echo "=== parseStringifiedDate implementation ==="
rg -n 'parseStringifiedDate' src/utils/ -A 10 -B 2

echo -e "\n=== Check how JSON body (non-form-data) is serialized ==="
rg -n 'JSON.stringify|JSON.parse' src/treaty2/index.ts -B 3 -A 2 | head -40

echo -e "\n=== Check treaty2-nested-form.test.ts for Date tests ==="
rg -n 'Date|date' test/treaty2-nested-form.test.ts -B 2 -A 2

echo -e "\n=== Check treaty2-files.test.ts for form-data tests ==="
rg -n 'Date|ArrayString|ObjectString' test/treaty2-files.test.ts -B 2 -A 2

Length of output: 3727


🏁 Script executed:

#!/bin/bash
# Search for actual test cases using Date in form-data contexts

echo "=== Full content of treaty2-nested-form.test.ts ==="
cat test/treaty2-nested-form.test.ts

echo -e "\n\n=== Full content of treaty2-files.test.ts ==="
head -100 test/treaty2-files.test.ts

echo -e "\n=== Check what isFile() does ==="
rg -n 'const isFile|function isFile' src/treaty2/index.ts -A 5

echo -e "\n=== Search for ArrayString/ObjectString schema definitions ==="
rg -n 'ArrayString|ObjectString' src/ -B 2 -A 2 | head -50

Length of output: 6841


Narrow shouldStringify to prevent unintended double-encoding of Date and non-plain objects in multipart form-data

The current shouldStringify implementation catches all non-null objects, including Date instances. This causes unintended double-encoding in form-data bodies:

  • Form-data: new Date()JSON.stringify()"\"2025-01-01T...Z\""
  • JSON body: new Date()JSON.stringify()"2025-01-01T...Z"

This is confirmed by parseStringifiedDate explicitly removing quotes (line 15 of parsingUtils.ts), which indicates the codebase already works around this issue. The proposal to limit stringification to plain objects and arrays, with special Date handling, is sound and avoids the unnecessary quoting while maintaining t.ArrayString and t.ObjectString behavior.

🤖 Prompt for AI Agents
src/treaty2/index.ts around lines 320-343, the current shouldStringify treats
every non-null object (including Date and other non-plain objects) as needing
JSON.stringify which causes double-encoded dates in multipart form-data; change
shouldStringify to only return true for plain objects and arrays (e.g.,
Array.isArray(value) || isPlainObject(value)), and update prepareValue to
explicitly handle Date (return value.toISOString()) before calling createNewFile
or JSON.stringify so Dates are serialized as ISO strings without extra quotes
and non-plain objects aren’t double-encoded.

Comment on lines 348 to 379
if (Array.isArray(field)) {
for (let i = 0; i < field.length; i++) {
const value = (field as any)[i]
// Check if array contains non-file objects
// If so, stringify the entire array (for t.ArrayString())
// Otherwise, append each element separately (for t.Files())
const hasNonFileObjects = field.some(
(item) =>
typeof item === 'object' &&
item !== null &&
!isFile(item)
)

if (hasNonFileObjects) {
// ArrayString case: stringify the whole array
formData.append(
key as any,
value instanceof File
? await createNewFile(value)
: value
JSON.stringify(field)
)
} else {
// Files case: append each element separately
for (let i = 0; i < field.length; i++) {
const value = (field as any)[i]
const preparedValue =
await prepareValue(value)

formData.append(
key as any,
preparedValue
)
}
}

continue
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Array heuristic may incorrectly JSON-stringify non-file object arrays for non-ArrayString schemas

The new array handling block:

if (Array.isArray(field)) {
    const hasNonFileObjects = field.some(
        (item) =>
            typeof item === 'object' &&
            item !== null &&
            !isFile(item)
    )

    if (hasNonFileObjects) {
        // ArrayString case: stringify the whole array
        formData.append(key as any, JSON.stringify(field))
    } else {
        // Files case: append each element separately
        for (let i = 0; i < field.length; i++) {
            const value = (field as any)[i]
            const preparedValue = await prepareValue(value)

            formData.append(key as any, preparedValue)
        }
    }

    continue
}

applies whenever hasFile(body) is true. This means:

  • Any array of non-file objects (e.g. t.Array(t.Object(...))) will be sent as a single JSON string field, not as multiple entries, as soon as the body also contains a file somewhere.
  • That behavior is perfect for t.ArrayString(...), but may break endpoints that intentionally use plain t.Array(t.Object(...)) + t.File() and expect the form-data representation they had before.

If the intention is to only auto-stringify for t.ArrayString, this heuristic is broader than that and may be a breaking change for existing consumers that mix files with object arrays.

Consider either:

  • Narrowing the condition so that only arrays of plain objects that are explicitly meant to be stringified are treated this way (if you can thread some schema metadata or convention down to this layer), or
  • At minimum, documenting this as a behavior change and adding explicit tests for t.Array(t.Object(...)) + t.File() so the new semantics are deliberate.

If you keep the current heuristic, it might be worth adding a quick comment here noting that all non-file-object arrays will be JSON-encoded when files are present, not just t.ArrayString cases.


I'll verify this review comment by examining the codebase context and understanding the schema handling. Let me gather information about how the array heuristic interacts with schema types.


🏁 Script executed:

#!/bin/bash
# Get broader context around the problematic code block
cat src/treaty2/index.ts | sed -n '300,400p'

Length of output: 4562


🏁 Script executed:

#!/bin/bash
# Find where hasFile is called and defined
rg -n "hasFile" src/treaty2/index.ts -B5 -A5

Length of output: 783


🏁 Script executed:

#!/bin/bash
# Look for ArrayString, Array, File schema definitions
fd -e ts -e js src/ | head -20 | xargs rg -l "ArrayString|t\.Array|t\.File" | head -10

Length of output: 515


🏁 Script executed:

#!/bin/bash
# Search for test files related to treaty2 and form data handling
fd -e test\. -e spec\. src/ | grep -i treaty

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Look for schema type information that might be available at this layer
rg -n "field.*schema\|schema.*type" src/treaty2/index.ts | head -20

Length of output: 0


I need to examine the test files and look for schema type handling. Let me fix the commands:


🏁 Script executed:

#!/bin/bash
# Search test files for ArrayString and file handling
rg -n "ArrayString\|t\.Array.*File\|File.*t\.Array" test/ -i -A3 -B3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at test files that might cover form data with mixed types
cat test/treaty2-files.test.ts | head -100

Length of output: 3093


🏁 Script executed:

#!/bin/bash
# Check for schema type definitions - look for t.ArrayString
rg -n "ArrayString" . --type ts --type js -C5 | head -50

Length of output: 3332


🏁 Script executed:

#!/bin/bash
# Look for where body/schema is processed before reaching this layer
rg -n "t\.Array\|t\.Object\|schema" src/treaty2/index.ts | head -30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check CHANGELOG to see if this is a recent change and what prompted it
cat CHANGELOG.md | head -150

Length of output: 4620


Let me examine the test file more comprehensively to see what scenarios are covered:


🏁 Script executed:

#!/bin/bash
# Get full treaty2-nested-form test file to see all test cases
cat test/treaty2-nested-form.test.ts

Length of output: 2516


🏁 Script executed:

#!/bin/bash
# Search for plain t.Array (not ArrayString) with files in tests
rg -n "t\.Array\(" test/ -A5 -B5 | grep -A10 -B10 "File\|file"

Length of output: 1081


🏁 Script executed:

#!/bin/bash
# Look for any test that combines t.Array with t.File or t.Files
rg -n "post.*body" test/treaty2-nested-form.test.ts -A10 | head -50

Length of output: 377


🏁 Script executed:

#!/bin/bash
# Check if there's any scenario testing Array of objects WITHOUT files
rg -n "variants\|metadata" test/treaty2-nested-form.test.ts -B5 -A5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search all test files for plain t.Array + File combinations (not ArrayString)
rg -n "t\.Array\(" test/ --type ts -A15 | grep -B15 "File\|Files"

Length of output: 855


🏁 Script executed:

#!/bin/bash
# Check if there are tests in treaty2.test.ts with object arrays
cat test/treaty2.test.ts | grep -A20 "t.Object"

Length of output: 7437


🏁 Script executed:

#!/bin/bash
# Search for any recent PR or comments about this heuristic
rg -n "hasNonFileObjects\|heuristic\|stringify.*array" . --type ts --type js

Length of output: 0


Heuristic for non-file object arrays is too broad and lacks test coverage for plain t.Array(t.Object(...)) + t.File() scenarios

The array handling logic at lines 349-364 stringifies any array containing non-file objects when hasFile(body) is true. However, this heuristic applies to all non-file-object arrays (e.g., t.Array(t.Object(...))), not just t.ArrayString(...) cases, because:

  • No schema metadata is available at this serialization layer to distinguish between them
  • The condition hasNonFileObjects is purely value-based and cannot determine schema intent
  • This will JSON-stringify plain object arrays whenever files are present in the body

This is a breaking change for existing consumers mixing t.Array(t.Object(...)) with t.File(). Test coverage only includes t.ArrayString + t.File() and t.ObjectString + t.File() scenarios; no tests cover plain t.Array(t.Object(...)) + t.File() combinations.

Required actions:

  1. Add explicit tests for t.Array(t.Object(...)) + t.File() to confirm expected behavior is intentional
  2. Either narrow the heuristic to only handle t.ArrayString cases (if schema metadata can be threaded down), or document this as a behavior change with clear migration guidance
  3. Add a code comment clarifying that all non-file-object arrays are JSON-encoded when files are present, not just t.ArrayString cases
🤖 Prompt for AI Agents
In src/treaty2/index.ts around lines 348-379 the value-based heuristic that
JSON-stringifies any array containing non-file objects is too broad and lacks
tests for plain t.Array(t.Object(...)) + t.File() cases; add explicit unit tests
covering t.Array(t.Object(...)) + t.File() to confirm behavior, then either (A)
narrow the heuristic to only stringify arrays known to be ArrayString by
threading schema metadata down to this serialization layer before making the
decision, or (B) if schema metadata cannot be made available, add a clear code
comment at this location stating that all non-file-object arrays will be
JSON-encoded when files are present and update the library documentation and
migration notes accordingly; choose one of the two fixes and implement the
corresponding tests and documentation updates so behavior and intent are
unambiguous.

@normalid-awa
Copy link

Really need this PR as my code base is now full of JSON.stringtify jank anywhere XDD

normalid-awa added a commit to normalid-awa/ipsc-scoreboard that referenced this pull request Nov 30, 2025
Add uspsa scoring method field

Add walkthrough time input
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants