uniweb 0.16.4 → 0.16.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.16.4",
3
+ "version": "0.16.5",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,14 +41,14 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "^0.8.4",
45
- "@uniweb/kit": "^0.11.2",
46
- "@uniweb/runtime": "^0.11.4"
44
+ "@uniweb/core": "^0.8.5",
45
+ "@uniweb/kit": "^0.11.3",
46
+ "@uniweb/runtime": "^0.11.5"
47
47
  },
48
48
  "peerDependencies": {
49
- "@uniweb/build": "^0.18.4",
49
+ "@uniweb/build": "^0.18.5",
50
50
  "@uniweb/content-reader": "^1.2.2",
51
- "@uniweb/semantic-parser": "^1.2.1"
51
+ "@uniweb/semantic-parser": "^1.2.2"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -18,6 +18,7 @@ import { hasUncommittedContent } from '../utils/git.js'
18
18
  import {
19
19
  backfillEntityUuids,
20
20
  writeSiteEntityUuid,
21
+ writeSiteOrg,
21
22
  emitSyncPackages,
22
23
  readZip,
23
24
  diffSiteUnits,
@@ -390,6 +391,59 @@ export function writeItemUuids(siteDir, map) {
390
391
  updateSyncCache(siteDir, { itemUuids: map })
391
392
  }
392
393
 
394
+ /**
395
+ * The org this site was created under, as `@handle`, or null.
396
+ *
397
+ * Read back from `site.yml::$org` (stored bare — see `writeSiteOrg`) and re-dressed
398
+ * with the `@` the CLI and the wire both use. Callers pass it as `--as-org`'s default
399
+ * so an org named once, at create, does not have to be re-typed on every later push.
400
+ *
401
+ * Deliberately NOT a fallback for the flag: an explicit `--as-org` always wins and
402
+ * rides verbatim, so this can only add a value where the CLI previously sent none.
403
+ *
404
+ * @param {string} siteDir
405
+ * @returns {string|null}
406
+ */
407
+ export function readSiteOrg(siteDir) {
408
+ try {
409
+ const y = yaml.load(readFileSync(join(siteDir, 'site.yml'), 'utf8'))
410
+ const h = y && typeof y === 'object' ? y.$org : null
411
+ return typeof h === 'string' && h.trim()
412
+ ? `@${h.trim().replace(/^@/, '')}`
413
+ : null
414
+ } catch {
415
+ return null
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Record the org a just-minted site was created under, if one was named.
421
+ *
422
+ * Only what we were TOLD is recorded — when no `--as-org` was passed the backend
423
+ * chose the owner and its create response carries no org, so there is nothing to
424
+ * write and guessing one would be worse than the gap. Returns the display form for
425
+ * the caller's "here's what resolved" line, or null when nothing was recorded.
426
+ *
427
+ * @param {string} siteDir
428
+ * @param {string|null|undefined} asOrg - the `--as-org` value, `@handle` or bare
429
+ * @returns {string|null}
430
+ */
431
+ function recordSiteOrg(siteDir, asOrg) {
432
+ const handle = String(asOrg || '')
433
+ .replace(/^@/, '')
434
+ .replace(/\/.*$/, '')
435
+ .trim()
436
+ if (!handle) return null
437
+ try {
438
+ writeSiteOrg(siteDir, handle)
439
+ return `@${handle}`
440
+ } catch {
441
+ // The uuid is the load-bearing back-fill; losing the org note must never
442
+ // fail a push that already succeeded on the backend.
443
+ return null
444
+ }
445
+ }
446
+
393
447
  /**
394
448
  * Guarantee the site EXISTS on the backend before anything is uploaded against it.
395
449
  *
@@ -429,7 +483,7 @@ export async function ensureSiteExists({
429
483
  /* unreadable site.yml — treat as un-synced and let the create decide */
430
484
  }
431
485
  if (typeof siteYml.$uuid === 'string') {
432
- return { uuid: siteYml.$uuid, created: false }
486
+ return { uuid: siteYml.$uuid, created: false, org: readSiteOrg(siteDir) }
433
487
  }
434
488
 
435
489
  // Both are required by the create. Catching it here turns a 400 into a sentence
@@ -484,8 +538,16 @@ export async function ensureSiteExists({
484
538
  // this point cannot leave a cache pointing at a different site with no way to
485
539
  // detect it.
486
540
  updateSyncCache(siteDir, { siteUuid: minted })
487
- note?.(`Created the site on the backend (recorded $uuid in site.yml).`)
488
- return { uuid: minted, created: true }
541
+ // Ownership is decided HERE and nowhere else — this is the one call that reads
542
+ // `as_org`. Recording it beside the uuid is what makes the answer readable later,
543
+ // and naming it now is the "show what resolved" half of the ask-once pattern.
544
+ const org = recordSiteOrg(siteDir, asOrg)
545
+ note?.(
546
+ org
547
+ ? `Created the site on the backend under ${org} (recorded $uuid + $org in site.yml).`
548
+ : `Created the site on the backend (recorded $uuid in site.yml).`
549
+ )
550
+ return { uuid: minted, created: true, org }
489
551
  }
490
552
 
491
553
  /**
@@ -838,6 +900,12 @@ export async function pushSyncPackages({
838
900
  updateSyncCache(siteDir, { siteUuid: minted })
839
901
  boundSiteUuid = minted
840
902
  wrote.push('recorded site $uuid in site.yml')
903
+ // The OTHER create path (a media-less push never reaches `ensureSiteExists`,
904
+ // which is gated on the site having local media). Both mint a site, so both
905
+ // owe the same record — recording it in only one place would make `$org`
906
+ // present or absent depending on whether the site happens to have images.
907
+ const createdOrg = recordSiteOrg(siteDir, asOrg)
908
+ if (createdOrg) wrote.push(`recorded site $org (${createdOrg}) in site.yml`)
841
909
  const createdFinalized = extractFinalized(payload)
842
910
  harvest(createdFinalized)
843
911
  siteFinalizedDoc = createdFinalized?.[0]?.document || null
@@ -56,6 +56,7 @@ import { detectWorkspacePm, installCmd } from '../utils/pm.js'
56
56
  import { BackendClient } from '../backend/client.js'
57
57
  import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
58
58
  import { extractFoundationRef } from '../utils/site-content-refs.js'
59
+ import { readUwxDocuments } from '../utils/uwx-read.js'
59
60
 
60
61
  const colors = {
61
62
  reset: '\x1b[0m',
@@ -189,11 +190,19 @@ export async function clone(args = [], deps = {}) {
189
190
  command: 'Cloning'
190
191
  })
191
192
 
192
- // 1. GET the site-content document (no @uniweb/build needed for a read).
193
+ // 1. GET the site-content document.
194
+ //
195
+ // ⛔ The body is a `.uwx` ZIP, not JSON — `application/vnd.uniweb.exchange.
196
+ // entity+zip`, on `content` and `folder` pulls alike. This used to call
197
+ // `res.json()`, which failed on the ZIP magic and reported *"Could not reach
198
+ // the backend: Unexpected token 'P'"* — blaming the network for a body we had
199
+ // received intact and mis-read. Decode with the local reader: this command runs
200
+ // before a project exists, so `@uniweb/build/uwx`'s `readZip` is out of reach
201
+ // (see `utils/uwx-read.js`).
193
202
  info(
194
203
  `Reading site ${colors.bright}${siteUuid}${colors.reset} from ${colors.dim}${client.origin}${colors.reset} …`
195
204
  )
196
- let payload
205
+ let documents
197
206
  try {
198
207
  const res = await client.pullSiteContent(siteUuid)
199
208
  if (res.status === 404) {
@@ -206,13 +215,14 @@ export async function clone(args = [], deps = {}) {
206
215
  note('Run `uniweb login` first (or pass --token <bearer>).')
207
216
  return { exitCode: 1 }
208
217
  }
209
- payload = await res.json()
218
+ documents = readUwxDocuments(Buffer.from(await res.arrayBuffer()))
210
219
  } catch (err) {
211
220
  error(`Could not reach the backend at ${client.origin}: ${err.message}`)
212
221
  return { exitCode: 1 }
213
222
  }
214
223
 
215
- const document = extractDocument(payload)
224
+ const document =
225
+ documents.map(extractDocument).find(Boolean) || null
216
226
  if (!document) {
217
227
  error('The site-content response carried no recognizable document.')
218
228
  return { exitCode: 1 }
@@ -28,6 +28,12 @@
28
28
  * uniweb publish --dry-run Resolve everything; POST nothing
29
29
  * uniweb publish --yes Skip confirmations (CI); never block on a prompt
30
30
  * uniweb publish --force Overwrite upstream app-side edits (drop the push gate)
31
+ * uniweb publish --as-org @org Publish under @org (membership-gated). Only the
32
+ * FIRST publish of a site reads it — that create is
33
+ * what decides which org owns the site and whose
34
+ * storage its assets are charged to. It is then
35
+ * recorded as `site.yml::$org` and replayed, so it
36
+ * never has to be re-typed.
31
37
  * uniweb publish --no-save Skip the deploy.yml lastDeploy auto-save
32
38
  * uniweb publish --backend <url> Override the backend origin
33
39
  * uniweb publish --token <bearer> Auth bearer (skips `uniweb login`)
@@ -66,7 +72,8 @@ import {
66
72
  ensureItemUuids,
67
73
  ensureSiteExists,
68
74
  clearRemoteSyncStateIfUnbound,
69
- pushSyncPackages
75
+ pushSyncPackages,
76
+ readSiteOrg
70
77
  } from '../backend/site-sync.js'
71
78
  import { uploadDataBundle } from '../backend/data-bundle.js'
72
79
  import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
@@ -167,11 +174,16 @@ async function persistLastDeploy(siteDir, opts) {
167
174
  export async function publish(args = []) {
168
175
  const dryRun = args.includes('--dry-run')
169
176
  const noSave = args.includes('--no-save')
170
- const asOrg = readFlagValue(args, '--as-org')
171
177
  const foundationDir = readFlagValue(args, '--foundation') // optional local foundation for Model schemas
172
178
 
173
179
  const siteDir = await resolveSiteDir(args, 'publish')
174
180
 
181
+ // The acting org: the flag verbatim, else the one this site was CREATED under
182
+ // (`site.yml::$org`). Only the create reads `as_org`, so replaying the recorded
183
+ // handle reasserts existing ownership rather than choosing new ownership. Absent
184
+ // both, no `as_org` is sent — unchanged from before the record existed.
185
+ const asOrg = readFlagValue(args, '--as-org') || readSiteOrg(siteDir)
186
+
175
187
  // Advisory only — warns and ships. See utils/conformance.js for why this
176
188
  // is not a gate.
177
189
  await warnIfContentDoesNotConform(siteDir, { args })
@@ -26,7 +26,11 @@
26
26
  *
27
27
  * Usage:
28
28
  * uniweb push Build, push both lanes, back-fill $uuid
29
- * uniweb push --as-org @org Act as @org (membership-gated)
29
+ * uniweb push --as-org @org Act as @org (membership-gated). Needed only
30
+ * for the FIRST push of a site — it decides
31
+ * which org owns it (and whose storage its
32
+ * assets are charged to). Recorded as
33
+ * `site.yml::$org` and replayed after that.
30
34
  * uniweb push --dry-run Report what would be pushed; submit nothing
31
35
  * uniweb push -o out.uwx Write the .uwx file(s) per lane; submit nothing
32
36
  * uniweb push --registry <url> Override the backend origin
@@ -68,7 +72,8 @@ import {
68
72
  ensureItemUuids,
69
73
  ensureSiteExists,
70
74
  clearRemoteSyncStateIfUnbound,
71
- pushSyncPackages
75
+ pushSyncPackages,
76
+ readSiteOrg
72
77
  } from '../backend/site-sync.js'
73
78
 
74
79
  // Re-exported for downstream importers (pull.js, push.test.js) that read these
@@ -106,7 +111,6 @@ export async function push(args = []) {
106
111
  const dryRun = args.includes('--dry-run')
107
112
  const output = flagValue(args, '-o') || flagValue(args, '--output')
108
113
  const tokenFlag = flagValue(args, '--token')
109
- const asOrg = flagValue(args, '--as-org')
110
114
  const foundationDir = flagValue(args, '--foundation')
111
115
  const sendAll = args.includes('--all') // bypass the send-only-changed cache
112
116
  // --force drops the optimistic-concurrency precondition, making the push
@@ -118,6 +122,13 @@ export async function push(args = []) {
118
122
 
119
123
  const siteDir = await resolveSiteDir(args, 'push')
120
124
 
125
+ // The acting org: the flag verbatim, else the one this site was CREATED under
126
+ // (`site.yml::$org`). The org is consumed by the create that mints `$uuid`, so
127
+ // a site that already exists is already owned — replaying the recorded handle
128
+ // reasserts that rather than choosing anything new. A site with no `$org`
129
+ // recorded sends no `as_org`, exactly as before.
130
+ const asOrg = flagValue(args, '--as-org') || readSiteOrg(siteDir)
131
+
121
132
  // Advisory only — warns and pushes. A malformed data block otherwise rides
122
133
  // the sync wire unchecked; see utils/conformance.js.
123
134
  await warnIfContentDoesNotConform(siteDir, { args })
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-11T13:40:25.377Z",
3
+ "generatedAt": "2026-08-12T22:01:32.128Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.18.4",
6
+ "version": "0.18.5",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -28,7 +28,7 @@
28
28
  "deps": []
29
29
  },
30
30
  "@uniweb/core": {
31
- "version": "0.8.4",
31
+ "version": "0.8.5",
32
32
  "path": "framework/core",
33
33
  "deps": [
34
34
  "@uniweb/semantic-parser",
@@ -46,7 +46,7 @@
46
46
  "deps": []
47
47
  },
48
48
  "@uniweb/kit": {
49
- "version": "0.11.2",
49
+ "version": "0.11.3",
50
50
  "path": "framework/kit",
51
51
  "deps": [
52
52
  "@uniweb/core",
@@ -65,7 +65,7 @@
65
65
  "deps": []
66
66
  },
67
67
  "@uniweb/projections": {
68
- "version": "0.2.7",
68
+ "version": "0.2.8",
69
69
  "path": "framework/projections",
70
70
  "deps": [
71
71
  "@uniweb/content-writer",
@@ -73,7 +73,7 @@
73
73
  ]
74
74
  },
75
75
  "@uniweb/runtime": {
76
- "version": "0.11.4",
76
+ "version": "0.11.5",
77
77
  "path": "framework/runtime",
78
78
  "deps": [
79
79
  "@uniweb/core",
@@ -96,7 +96,7 @@
96
96
  "deps": []
97
97
  },
98
98
  "@uniweb/semantic-parser": {
99
- "version": "1.2.1",
99
+ "version": "1.2.2",
100
100
  "path": "framework/semantic-parser",
101
101
  "deps": []
102
102
  },
@@ -111,7 +111,7 @@
111
111
  "deps": []
112
112
  },
113
113
  "@uniweb/unipress": {
114
- "version": "0.8.3",
114
+ "version": "0.8.4",
115
115
  "path": "framework/unipress",
116
116
  "deps": [
117
117
  "@uniweb/build",
package/src/index.js CHANGED
@@ -1318,6 +1318,10 @@ ${colors.bright}Options:${colors.reset}
1318
1318
  --yes Skip confirmations (CI); never block on a prompt
1319
1319
  --no-save Skip the deploy.yml lastDeploy auto-save
1320
1320
  --no-validate Skip the content-conformance check (it only warns)
1321
+ --as-org @org Publish under @org (membership-gated). Read only on a site's
1322
+ FIRST publish — that create decides which org owns the site,
1323
+ and whose storage its assets are charged to. Recorded as
1324
+ site.yml \$org and replayed, so it is never re-typed.
1321
1325
  --backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
1322
1326
  --token <bearer> Auth bearer (skips \`uniweb login\`)
1323
1327
  `,
@@ -1693,6 +1697,7 @@ ${colors.bright}Global Options:${colors.reset}
1693
1697
  ${colors.bright}Publish Options:${colors.reset}
1694
1698
  --dry-run Resolve everything; release/sync/POST nothing
1695
1699
  --yes Skip confirmations (CI); never block on a prompt
1700
+ --as-org @org Publish under @org (first publish only; then remembered)
1696
1701
  --no-save Skip the deploy.yml lastDeploy auto-save
1697
1702
  --no-validate Skip the content-conformance check (it only warns)
1698
1703
  --backend <url> Backend origin (default: \$UNIWEB_REGISTER_URL or built-in)
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Minimal `.uwx` reader — for the commands that run BEFORE a project exists.
3
+ *
4
+ * ## Why this exists rather than importing the real one
5
+ *
6
+ * Every other `.uwx` reader in the CLI (`pull.js`, `site-sync.js`, `content.js`)
7
+ * imports `readZip` from `@uniweb/build/uwx`. That is correct for them: they run
8
+ * *inside* a project, where `@uniweb/build` resolves from the project's own
9
+ * `node_modules`.
10
+ *
11
+ * **`@uniweb/build` is not a dependency of this package** (see `package.json`),
12
+ * so a command that runs where no project exists cannot reach it — statically or
13
+ * dynamically. `uniweb clone` is exactly that command: its whole job is to turn a
14
+ * backend site into a local project that does not exist yet.
15
+ *
16
+ * So this is a deliberate second implementation, kept minimal and dependency-free
17
+ * (`node:zlib` is a builtin), the same way `clone.js` keeps its own
18
+ * `extractDocument` for the same reason. **Do not import this from a command that
19
+ * runs inside a project** — use `@uniweb/build/uwx`'s `readZip` there, so the
20
+ * producer and the consumer of a `.uwx` stay one implementation wherever they can.
21
+ *
22
+ * ## Two things measured on a real payload, both of which would break a naive reader
23
+ *
24
+ * 1. ⛔ **The archive is NOT "stored".** `pull.js` describes the format as *"our
25
+ * Stored ZIP"*, and that is true of `manifest.json` and false of the entity
26
+ * files — measured on a live pull: `manifest.json` method 0 (STORED), the
27
+ * entity JSON method 8 (DEFLATED). A stored-only reader silently yields the
28
+ * manifest and drops the document, i.e. it returns success and no entities.
29
+ * **Both methods are handled below and the tests cover a mixed archive.**
30
+ * 2. The entity payload is the *only* thing wanted; `manifest.json` is skipped, as
31
+ * every other reader in the tree does.
32
+ *
33
+ * We read the **central directory** rather than walking local file headers,
34
+ * because a local header may carry zeroed sizes when the general-purpose bit 3
35
+ * flag defers them to a trailing data descriptor. The central directory always
36
+ * carries the real sizes.
37
+ */
38
+
39
+ import { inflateRawSync } from 'node:zlib'
40
+
41
+ const SIG_EOCD = 0x06054b50
42
+ const SIG_CENTRAL = 0x02014b50
43
+ const SIG_LOCAL = 0x04034b50
44
+ const METHOD_STORED = 0
45
+ const METHOD_DEFLATED = 8
46
+
47
+ /** ZIP local-file-header magic, "PK\x03\x04" — the first two bytes are enough. */
48
+ export function looksLikeZip(buf) {
49
+ return Boolean(buf) && buf.length >= 2 && buf[0] === 0x50 && buf[1] === 0x4b
50
+ }
51
+
52
+ /**
53
+ * Locate the end-of-central-directory record.
54
+ *
55
+ * Scanned backwards because the record is last and variable-length (it carries an
56
+ * optional trailing comment).
57
+ *
58
+ * @param {Buffer} buf
59
+ * @returns {number} offset of the EOCD, or -1
60
+ */
61
+ function findEocd(buf) {
62
+ const min = Math.max(0, buf.length - 22 - 0xffff)
63
+ for (let i = buf.length - 22; i >= min; i--) {
64
+ if (buf.readUInt32LE(i) === SIG_EOCD) return i
65
+ }
66
+ return -1
67
+ }
68
+
69
+ /**
70
+ * Read every entry out of a ZIP as `[name, Buffer]` pairs.
71
+ *
72
+ * @param {Buffer} buf
73
+ * @returns {Array<[string, Buffer]>} empty when the buffer is not a readable ZIP
74
+ */
75
+ export function readUwxZip(buf) {
76
+ if (!looksLikeZip(buf)) return []
77
+ const eocd = findEocd(buf)
78
+ if (eocd < 0) return []
79
+
80
+ const count = buf.readUInt16LE(eocd + 10)
81
+ let ptr = buf.readUInt32LE(eocd + 16)
82
+ const out = []
83
+
84
+ for (let i = 0; i < count; i++) {
85
+ if (ptr + 46 > buf.length || buf.readUInt32LE(ptr) !== SIG_CENTRAL) break
86
+
87
+ const method = buf.readUInt16LE(ptr + 10)
88
+ const csize = buf.readUInt32LE(ptr + 20)
89
+ const nameLen = buf.readUInt16LE(ptr + 28)
90
+ const extraLen = buf.readUInt16LE(ptr + 30)
91
+ const commentLen = buf.readUInt16LE(ptr + 32)
92
+ const localOff = buf.readUInt32LE(ptr + 42)
93
+ const name = buf.subarray(ptr + 46, ptr + 46 + nameLen).toString('utf8')
94
+
95
+ // The local header's name/extra lengths are independent of the central
96
+ // directory's — read them where the data actually starts, not from above.
97
+ if (localOff + 30 <= buf.length && buf.readUInt32LE(localOff) === SIG_LOCAL) {
98
+ const lNameLen = buf.readUInt16LE(localOff + 26)
99
+ const lExtraLen = buf.readUInt16LE(localOff + 28)
100
+ const start = localOff + 30 + lNameLen + lExtraLen
101
+ const raw = buf.subarray(start, start + csize)
102
+ try {
103
+ if (method === METHOD_STORED) out.push([name, Buffer.from(raw)])
104
+ else if (method === METHOD_DEFLATED) out.push([name, inflateRawSync(raw)])
105
+ // any other method: skip rather than guess
106
+ } catch {
107
+ /* a corrupt entry must not lose the readable ones */
108
+ }
109
+ }
110
+
111
+ ptr += 46 + nameLen + extraLen + commentLen
112
+ }
113
+
114
+ return out
115
+ }
116
+
117
+ /**
118
+ * The entity `$`-documents inside a `.uwx`, or a JSON body's documents.
119
+ *
120
+ * Mirrors `pull.js`'s `readPullDocuments` in behaviour — including its JSON
121
+ * fallback, so a future envelope change does not break this lane either — but
122
+ * without the `@uniweb/build` import. See the header for why that matters.
123
+ *
124
+ * @param {Buffer} buf
125
+ * @returns {object[]} parsed documents, possibly empty
126
+ */
127
+ export function readUwxDocuments(buf) {
128
+ if (!buf || buf.length === 0) return []
129
+
130
+ if (looksLikeZip(buf)) {
131
+ const docs = []
132
+ for (const [name, data] of readUwxZip(buf)) {
133
+ if (name === 'manifest.json' || !name.endsWith('.json')) continue
134
+ try {
135
+ docs.push(JSON.parse(data.toString('utf8')))
136
+ } catch {
137
+ /* skip a non-document entry */
138
+ }
139
+ }
140
+ return docs
141
+ }
142
+
143
+ let payload
144
+ try {
145
+ payload = JSON.parse(buf.toString('utf8'))
146
+ } catch {
147
+ return []
148
+ }
149
+ if (Array.isArray(payload)) return payload.filter(Boolean)
150
+ const list = Array.isArray(payload?.entities)
151
+ ? payload.entities
152
+ : Array.isArray(payload?.documents)
153
+ ? payload.documents
154
+ : null
155
+ if (list) return list.filter(Boolean)
156
+ return payload ? [payload] : []
157
+ }