form0-core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 paqu.io
3
+ Copyright (c) 2025-present paqu.io
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,28 +2,27 @@
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/form0-core)](https://www.npmjs.com/package/form0-core)
4
4
  [![NPM Downloads](https://img.shields.io/npm/dt/form0-core)](https://www.npmjs.com/package/form0-core)
5
- [![License](https://img.shields.io/npm/l/form0-core)](LICENSE)
5
+ ![NPM License](https://img.shields.io/npm/l/form0-core)
6
6
  [![Docs](https://img.shields.io/badge/docs-docs.form0.dev-2563eb)](https://docs.form0.dev)
7
7
  [![Website](https://img.shields.io/badge/site-form0.dev-0f172a)](https://form0.dev)
8
+ ![NPM Last Update](https://img.shields.io/npm/last-update/form0-core)
8
9
 
9
10
  > [!WARNING]
10
11
  > form0 is in active, very early development. Do not use in production. Expect breaking
11
12
  > changes and unstable behavior.
12
13
 
13
- form0-core is the schema-driven engine that powers form0 ecosystem. It is framework-agnostic and runs in any JavaScript runtime (Node.js, browsers, React Native, etc.)
14
+ form0-core is the schema-driven engine that powers [form0 open-source ecosystem](https://form0.dev). It is framework-agnostic and runs in any JavaScript runtime (Node.js, browsers, React Native, etc.)
14
15
 
15
16
  ## 🚀 Start with the CLI (recommended)
16
17
 
17
- The entry point for most users is form0-cli. Follow the quickstart to create a project and preview
18
- your schema:
19
- - https://docs.form0.dev/getting-started/quickstart
18
+ The entry point for most users is [form0-cli](https://github.com/paqu-io/form0-cli). Follow the [quickstart](https://docs.form0.dev/getting-started/quickstart) to create a project and preview
19
+ your schema.
20
20
 
21
21
  ## 🗂️ Documentation
22
22
 
23
- - Docs home: https://docs.form0.dev
24
- - form0-core overview: https://docs.form0.dev/core/overview
25
- - form0-core concepts: https://docs.form0.dev/core/concepts
26
- - Main site: https://form0.dev
23
+ - Overview: https://docs.form0.dev/core/overview
24
+ - Concepts: https://docs.form0.dev/core/concepts
25
+ - Full docs: https://docs.form0.dev
27
26
 
28
27
  ## Direct usage (advanced)
29
28
 
@@ -36,3 +35,17 @@ npm install form0-core
36
35
  ## Security
37
36
 
38
37
  See `SECURITY.md` for security modes and configuration.
38
+
39
+ ## Requirements
40
+
41
+ - Node.js 18+
42
+
43
+ ## Related repositories
44
+
45
+ - [form0-cli](https://github.com/paqu-io/form0-cli) - Command-line interface
46
+ - [form0-react](https://github.com/paqu-io/form0-react) - React components
47
+ - [form0-react-native](https://github.com/paqu-io/form0-react-native) - React Native components
48
+
49
+ ## Contributing
50
+
51
+ Contributions are welcome! Please feel free to submit [issues](https://github.com/paqu-io/form0-core/issues) and [pull requests](https://github.com/paqu-io/form0-core/pulls).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -43,7 +43,7 @@
43
43
  "bugs": {
44
44
  "url": "https://github.com/paqu-io/form0-core/issues"
45
45
  },
46
- "homepage": "https://form0.dev/",
46
+ "homepage": "https://form0.dev",
47
47
  "engines": {
48
48
  "node": ">=18.0.0"
49
49
  },
@@ -199,6 +199,12 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
199
199
  sectionFields,
200
200
  } = buildRepeatableMetadata(elementsSource);
201
201
 
202
+ const hasStructuredRepeatableState =
203
+ state &&
204
+ Object.prototype.hasOwnProperty.call(state, 'repeatable') &&
205
+ state.repeatable &&
206
+ typeof state.repeatable === 'object';
207
+
202
208
  // Transform the values object using the preferred keys
203
209
  for (const [dataName, value] of Object.entries(state.values)) {
204
210
  // Skip flattened choice fields (created by form renderer for allow_other fields)
@@ -292,7 +298,9 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
292
298
  const childIds = getNestedChildIds([...currentPath, pathKey]);
293
299
 
294
300
  // Structured RepeatableSection state supplied (new format)
295
- if (Array.isArray(structuredInstances) && structuredInstances.length > 0) {
301
+ // Empty arrays are treated as explicit "no instances" and must not fall back
302
+ // to legacy flattened behavior, which can emit phantom records.
303
+ if (Array.isArray(structuredInstances)) {
296
304
  const recordCount = Math.max(structuredInstances.length, childIds.length);
297
305
  const childRecords = [];
298
306
 
@@ -360,16 +368,20 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
360
368
  }
361
369
 
362
370
  for (const [, childRepInfo] of repInfo.children) {
363
- const nestedInstances = instanceRepeatable[childRepInfo.preferredKey] || [];
371
+ const rawNestedInstances = Object.prototype.hasOwnProperty.call(
372
+ instanceRepeatable,
373
+ childRepInfo.preferredKey
374
+ )
375
+ ? instanceRepeatable[childRepInfo.preferredKey]
376
+ : [];
377
+ const nestedInstances = Array.isArray(rawNestedInstances) ? rawNestedInstances : [];
364
378
  const childRepeatableArray = processRepeatableSection(
365
379
  childRepInfo,
366
380
  [...currentPath, pathKey],
367
381
  nestedInstances
368
382
  );
369
- if (childRepeatableArray.length > 0) {
370
- const childOutputKey = resolveRepeatableOutputKey(childRepInfo);
371
- childRecord.form_values[childOutputKey] = childRepeatableArray;
372
- }
383
+ const childOutputKey = resolveRepeatableOutputKey(childRepInfo);
384
+ childRecord.form_values[childOutputKey] = childRepeatableArray;
373
385
  }
374
386
 
375
387
  if (hasMeaningfulRecord(childRecord)) {
@@ -501,14 +513,23 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
501
513
  for (const [, repInfo] of repeatableSectionTree) {
502
514
  if (repInfo.parentPath.length === 0) {
503
515
  // This is a top-level RepeatableSection
504
- const structuredInstances =
505
- state.repeatable && state.repeatable[repInfo.preferredKey]
516
+ const rawStructuredInstances = hasStructuredRepeatableState
517
+ ? Object.prototype.hasOwnProperty.call(state.repeatable, repInfo.preferredKey)
506
518
  ? state.repeatable[repInfo.preferredKey]
507
- : null;
519
+ : []
520
+ : null;
521
+ const structuredInstances = hasStructuredRepeatableState
522
+ ? Array.isArray(rawStructuredInstances)
523
+ ? rawStructuredInstances
524
+ : []
525
+ : null;
508
526
 
509
527
  const childRecords = processRepeatableSection(repInfo, [], structuredInstances);
510
- if (childRecords.length > 0) {
511
- const repeatableOutputKey = resolveRepeatableOutputKey(repInfo);
528
+ const repeatableOutputKey = resolveRepeatableOutputKey(repInfo);
529
+ if (hasStructuredRepeatableState) {
530
+ // In structured mode keep explicit repeatable keys, even when empty.
531
+ form_values[repeatableOutputKey] = childRecords;
532
+ } else if (childRecords.length > 0) {
512
533
  form_values[repeatableOutputKey] = childRecords;
513
534
  }
514
535
  }