houdini 0.17.4 → 0.17.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.
Files changed (35) hide show
  1. package/.turbo/turbo-compile.log +2 -2
  2. package/.turbo/turbo-typedefs.log +2 -2
  3. package/CHANGELOG.md +6 -0
  4. package/build/cmd-cjs/index.js +9087 -10224
  5. package/build/cmd-esm/index.js +9087 -10224
  6. package/build/codegen/transforms/paginate.d.ts +10 -11
  7. package/build/codegen-cjs/index.js +9092 -10229
  8. package/build/codegen-esm/index.js +9092 -10229
  9. package/build/lib-cjs/index.js +9221 -10358
  10. package/build/lib-esm/index.js +9221 -10358
  11. package/build/runtime/lib/network.d.ts +1 -0
  12. package/build/runtime/lib/networkUtils.d.ts +8 -0
  13. package/build/runtime-cjs/lib/network.d.ts +1 -0
  14. package/build/runtime-cjs/lib/network.js +33 -1
  15. package/build/runtime-cjs/lib/networkUtils.d.ts +8 -0
  16. package/build/runtime-cjs/lib/networkUtils.js +85 -0
  17. package/build/runtime-esm/lib/network.d.ts +1 -0
  18. package/build/runtime-esm/lib/network.js +33 -1
  19. package/build/runtime-esm/lib/networkUtils.d.ts +8 -0
  20. package/build/runtime-esm/lib/networkUtils.js +60 -0
  21. package/build/test-cjs/index.js +9091 -10228
  22. package/build/test-esm/index.js +9091 -10228
  23. package/build/vite-cjs/index.js +9149 -10286
  24. package/build/vite-esm/index.js +9149 -10286
  25. package/package.json +2 -2
  26. package/src/codegen/generators/artifacts/artifacts.test.ts +99 -66
  27. package/src/codegen/generators/artifacts/pagination.test.ts +12 -8
  28. package/src/codegen/generators/artifacts/policy.test.ts +12 -8
  29. package/src/codegen/generators/definitions/schema.test.ts +12 -36
  30. package/src/codegen/generators/persistedQueries/persistedQuery.test.ts +2 -2
  31. package/src/codegen/transforms/fragmentVariables.test.ts +24 -16
  32. package/src/codegen/transforms/paginate.test.ts +9 -6
  33. package/src/codegen/transforms/paginate.ts +2 -2
  34. package/src/runtime/lib/network.ts +58 -1
  35. package/src/runtime/lib/networkUtils.ts +151 -0
@@ -29,7 +29,7 @@ test('cache policy is persisted in artifact', async function () {
29
29
  export default {
30
30
  name: "CachedFriends",
31
31
  kind: "HoudiniQuery",
32
- hash: "72a09504e4d65757a4277e3ef95cd93788b7f918519924901c3d5d7d39a4d32a",
32
+ hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
33
33
 
34
34
  raw: \`query CachedFriends {
35
35
  user {
@@ -38,7 +38,8 @@ test('cache policy is persisted in artifact', async function () {
38
38
  }
39
39
  id
40
40
  }
41
- }\`,
41
+ }
42
+ \`,
42
43
 
43
44
  rootType: "Query",
44
45
 
@@ -103,7 +104,7 @@ test('can change default cache policy', async function () {
103
104
  export default {
104
105
  name: "CachedFriends",
105
106
  kind: "HoudiniQuery",
106
- hash: "72a09504e4d65757a4277e3ef95cd93788b7f918519924901c3d5d7d39a4d32a",
107
+ hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
107
108
 
108
109
  raw: \`query CachedFriends {
109
110
  user {
@@ -112,7 +113,8 @@ test('can change default cache policy', async function () {
112
113
  }
113
114
  id
114
115
  }
115
- }\`,
116
+ }
117
+ \`,
116
118
 
117
119
  rootType: "Query",
118
120
 
@@ -172,7 +174,7 @@ test('partial opt-in is persisted', async function () {
172
174
  export default {
173
175
  name: "CachedFriends",
174
176
  kind: "HoudiniQuery",
175
- hash: "72a09504e4d65757a4277e3ef95cd93788b7f918519924901c3d5d7d39a4d32a",
177
+ hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
176
178
 
177
179
  raw: \`query CachedFriends {
178
180
  user {
@@ -181,7 +183,8 @@ test('partial opt-in is persisted', async function () {
181
183
  }
182
184
  id
183
185
  }
184
- }\`,
186
+ }
187
+ \`,
185
188
 
186
189
  rootType: "Query",
187
190
 
@@ -246,7 +249,7 @@ test('can set default partial opt-in', async function () {
246
249
  export default {
247
250
  name: "CachedFriends",
248
251
  kind: "HoudiniQuery",
249
- hash: "72a09504e4d65757a4277e3ef95cd93788b7f918519924901c3d5d7d39a4d32a",
252
+ hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
250
253
 
251
254
  raw: \`query CachedFriends {
252
255
  user {
@@ -255,7 +258,8 @@ test('can set default partial opt-in', async function () {
255
258
  }
256
259
  id
257
260
  }
258
- }\`,
261
+ }
262
+ \`,
259
263
 
260
264
  rootType: "Query",
261
265
 
@@ -39,14 +39,10 @@ test('adds internal documents to schema', async function () {
39
39
  """
40
40
  directive @paginate(name: String) on FIELD
41
41
 
42
- """
43
- @prepend is used to tell the runtime to add the result to the end of the list
44
- """
42
+ """@prepend is used to tell the runtime to add the result to the end of the list"""
45
43
  directive @prepend(parentID: ID) on FRAGMENT_SPREAD
46
44
 
47
- """
48
- @append is used to tell the runtime to add the result to the start of the list
49
- """
45
+ """@append is used to tell the runtime to add the result to the start of the list"""
50
46
  directive @append(parentID: ID) on FRAGMENT_SPREAD
51
47
 
52
48
  """
@@ -55,14 +51,10 @@ test('adds internal documents to schema', async function () {
55
51
  """
56
52
  directive @parentID(value: ID!) on FRAGMENT_SPREAD
57
53
 
58
- """
59
- @when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
60
- """
54
+ """@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
61
55
  directive @when on FRAGMENT_SPREAD
62
56
 
63
- """
64
- @when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
65
- """
57
+ """@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
66
58
  directive @when_not on FRAGMENT_SPREAD
67
59
 
68
60
  """@arguments is used to define the arguments of a fragment"""
@@ -114,14 +106,10 @@ test('list operations are included', async function () {
114
106
  """
115
107
  directive @paginate(name: String) on FIELD
116
108
 
117
- """
118
- @prepend is used to tell the runtime to add the result to the end of the list
119
- """
109
+ """@prepend is used to tell the runtime to add the result to the end of the list"""
120
110
  directive @prepend(parentID: ID) on FRAGMENT_SPREAD
121
111
 
122
- """
123
- @append is used to tell the runtime to add the result to the start of the list
124
- """
112
+ """@append is used to tell the runtime to add the result to the start of the list"""
125
113
  directive @append(parentID: ID) on FRAGMENT_SPREAD
126
114
 
127
115
  """
@@ -130,14 +118,10 @@ test('list operations are included', async function () {
130
118
  """
131
119
  directive @parentID(value: ID!) on FRAGMENT_SPREAD
132
120
 
133
- """
134
- @when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
135
- """
121
+ """@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
136
122
  directive @when on FRAGMENT_SPREAD
137
123
 
138
- """
139
- @when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
140
- """
124
+ """@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
141
125
  directive @when_not on FRAGMENT_SPREAD
142
126
 
143
127
  """@arguments is used to define the arguments of a fragment"""
@@ -208,14 +192,10 @@ test("writing twice doesn't duplicate definitions", async function () {
208
192
  """
209
193
  directive @paginate(name: String) on FIELD
210
194
 
211
- """
212
- @prepend is used to tell the runtime to add the result to the end of the list
213
- """
195
+ """@prepend is used to tell the runtime to add the result to the end of the list"""
214
196
  directive @prepend(parentID: ID) on FRAGMENT_SPREAD
215
197
 
216
- """
217
- @append is used to tell the runtime to add the result to the start of the list
218
- """
198
+ """@append is used to tell the runtime to add the result to the start of the list"""
219
199
  directive @append(parentID: ID) on FRAGMENT_SPREAD
220
200
 
221
201
  """
@@ -224,14 +204,10 @@ test("writing twice doesn't duplicate definitions", async function () {
224
204
  """
225
205
  directive @parentID(value: ID!) on FRAGMENT_SPREAD
226
206
 
227
- """
228
- @when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
229
- """
207
+ """@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
230
208
  directive @when on FRAGMENT_SPREAD
231
209
 
232
- """
233
- @when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
234
- """
210
+ """@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
235
211
  directive @when_not on FRAGMENT_SPREAD
236
212
 
237
213
  """@arguments is used to define the arguments of a fragment"""
@@ -19,8 +19,8 @@ test('generates an artifact for every document', async function () {
19
19
 
20
20
  expect(JSON.parse((await fs.readFile(config.persistedQueryPath))!)).toMatchInlineSnapshot(`
21
21
  {
22
- "5ba2c5763a93559769c84d7dd536becf80b4c7be6db9f6af232a4309ebff665c": "query TestQuery1 {\\n version\\n}",
23
- "eb8e226b085997a05050d4407492d37aaf133c02186dfcdb75b8bebdb2d8d8fb": "query TestQuery2 {\\n user {\\n ...TestFragment\\n id\\n }\\n}\\n\\nfragment TestFragment on User {\\n firstName\\n}"
22
+ "361432f464ed44eed788f3ea66c4dabc46437b88edbe7daccca87045fd31447f": "query TestQuery1 {\\n version\\n}\\n",
23
+ "17f12389123502b3d5d81202d0af249bdf0ec95cea480c9c12501ef627abd463": "query TestQuery2 {\\n user {\\n ...TestFragment\\n id\\n }\\n}\\n\\nfragment TestFragment on User {\\n firstName\\n}\\n"
24
24
  }
25
25
  `)
26
26
  })
@@ -43,7 +43,7 @@ test('pass argument values to generated fragments', async function () {
43
43
  export default {
44
44
  name: "AllUsers",
45
45
  kind: "HoudiniQuery",
46
- hash: "1e1735126b2b1cc305ad7477fdd7e670e53f2bf58b2861b904ff2c076e9545ba",
46
+ hash: "c346b9eaafaa74d18a267a74706e193e8080b9533d994d6e8489d7e5b534ee41",
47
47
 
48
48
  raw: \`query AllUsers {
49
49
  ...QueryFragment_10b3uv
@@ -53,7 +53,8 @@ test('pass argument values to generated fragments', async function () {
53
53
  users(stringValue: "Hello") {
54
54
  id
55
55
  }
56
- }\`,
56
+ }
57
+ \`,
57
58
 
58
59
  rootType: "Query",
59
60
 
@@ -115,7 +116,7 @@ test("nullable arguments with no values don't show up in the query", async funct
115
116
  export default {
116
117
  name: "AllUsers",
117
118
  kind: "HoudiniQuery",
118
- hash: "f09634eff790efeeeac358898927bb3de4f6e9d399ef2edce6b676bdd6990a34",
119
+ hash: "19b6a6cc9d06ab798cbf4b0a9530e07a3473b78e7d964cc9d6557d8240ed9012",
119
120
 
120
121
  raw: \`query AllUsers {
121
122
  ...QueryFragment
@@ -125,7 +126,8 @@ test("nullable arguments with no values don't show up in the query", async funct
125
126
  users {
126
127
  id
127
128
  }
128
- }\`,
129
+ }
130
+ \`,
129
131
 
130
132
  rootType: "Query",
131
133
 
@@ -187,7 +189,7 @@ test("fragment arguments with default values don't rename the fragment", async f
187
189
  export default {
188
190
  name: "AllUsers",
189
191
  kind: "HoudiniQuery",
190
- hash: "075b137c07d965a6314f2e26317caaaf46b4a9dad4028aed011ab1bc08984848",
192
+ hash: "3835ee68277547d738cc8fd5051fe98799b5bd470516146906fa0f134a2b3891",
191
193
 
192
194
  raw: \`query AllUsers {
193
195
  ...QueryFragment
@@ -197,7 +199,8 @@ test("fragment arguments with default values don't rename the fragment", async f
197
199
  users(stringValue: "Hello") {
198
200
  id
199
201
  }
200
- }\`,
202
+ }
203
+ \`,
201
204
 
202
205
  rootType: "Query",
203
206
 
@@ -267,7 +270,7 @@ test('thread query variables to inner fragments', async function () {
267
270
  export default {
268
271
  name: "AllUsers",
269
272
  kind: "HoudiniQuery",
270
- hash: "1b7f099e8c65dc28bbe8b2f1fbb40560a48cc556a16e379bd3c538f41c53a2ed",
273
+ hash: "8fa4273ab75455c901e7de893f72a28af4c001afbf204ceca2fd7ab30b7ff372",
271
274
 
272
275
  raw: \`query AllUsers($name: String!) {
273
276
  ...QueryFragment_VDHGm
@@ -281,7 +284,8 @@ test('thread query variables to inner fragments', async function () {
281
284
  users(stringValue: $name) {
282
285
  id
283
286
  }
284
- }\`,
287
+ }
288
+ \`,
285
289
 
286
290
  rootType: "Query",
287
291
 
@@ -359,7 +363,7 @@ test('inner fragment with intermediate default value', async function () {
359
363
  export default {
360
364
  name: "AllUsers",
361
365
  kind: "HoudiniQuery",
362
- hash: "90f7cd2265f1a788dd7c2a8e4a6a43daf1f606fc7f37fd4ddd8157bb28c799f0",
366
+ hash: "d5753a3cae56b8133c72527cdccdd0c001effb48104b98806ac62dd9afeeb259",
363
367
 
364
368
  raw: \`query AllUsers {
365
369
  ...QueryFragment
@@ -373,7 +377,8 @@ test('inner fragment with intermediate default value', async function () {
373
377
  users(stringValue: "Hello", intValue: 2) {
374
378
  id
375
379
  }
376
- }\`,
380
+ }
381
+ \`,
377
382
 
378
383
  rootType: "Query",
379
384
 
@@ -443,7 +448,7 @@ test("default values don't overwrite unless explicitly passed", async function (
443
448
  export default {
444
449
  name: "AllUsers",
445
450
  kind: "HoudiniQuery",
446
- hash: "d2a76fb293043d9d6fb2264237a5468bee7d974d93af7495a2cb7225cc952b8b",
451
+ hash: "b155b401cdbdfe0f63dd47575fbcfb2aa90678e7530b93476c4efe559405cf4f",
447
452
 
448
453
  raw: \`query AllUsers {
449
454
  ...QueryFragment
@@ -457,7 +462,8 @@ test("default values don't overwrite unless explicitly passed", async function (
457
462
  users(stringValue: "Goodbye", intValue: 10) {
458
463
  id
459
464
  }
460
- }\`,
465
+ }
466
+ \`,
461
467
 
462
468
  rootType: "Query",
463
469
 
@@ -519,7 +525,7 @@ test('default arguments', async function () {
519
525
  export default {
520
526
  name: "AllUsers",
521
527
  kind: "HoudiniQuery",
522
- hash: "59f8191d32aebd9a49c3ea3d7c209f6ba63e905b91b5ac7b4ef6a02b8ff7e7af",
528
+ hash: "5c4a8d1fe2e117286ecdfbd273bf1beb2f71a0a3fd9ea6bc84fe97c394c1a836",
523
529
 
524
530
  raw: \`query AllUsers {
525
531
  ...QueryFragment
@@ -529,7 +535,8 @@ test('default arguments', async function () {
529
535
  users(boolValue: true, stringValue: "Hello") {
530
536
  id
531
537
  }
532
- }\`,
538
+ }
539
+ \`,
533
540
 
534
541
  rootType: "Query",
535
542
 
@@ -591,7 +598,7 @@ test('multiple with directives - no overlap', async function () {
591
598
  export default {
592
599
  name: "AllUsers",
593
600
  kind: "HoudiniQuery",
594
- hash: "b78aef48c431b5b61fde1ddc8be635b07b1972d1f1454d53c80ac80c4bbbc36b",
601
+ hash: "7327e6f7f6c8339feebb640b995c3e25efe1b25de29b1f43cb55c2a0566f713f",
595
602
 
596
603
  raw: \`query AllUsers {
597
604
  ...QueryFragment_2prn0K
@@ -601,7 +608,8 @@ test('multiple with directives - no overlap', async function () {
601
608
  users(boolValue: false, stringValue: "Goodbye") {
602
609
  id
603
610
  }
604
- }\`,
611
+ }
612
+ \`,
605
613
 
606
614
  rootType: "Query",
607
615
 
@@ -450,7 +450,7 @@ test('embeds node pagination query as a separate document', async function () {
450
450
  export default {
451
451
  name: "UserFriends_Pagination_Query",
452
452
  kind: "HoudiniQuery",
453
- hash: "c93f41015e8e748dd6a1b23a3053cd608c6fc86feeb99fb82d784a8f875836c9",
453
+ hash: "bb5131f921805b85c17e7b882f4ad66a9dad452d0f66534a1c8b8f9942adec48",
454
454
 
455
455
  refetch: {
456
456
  update: "append",
@@ -491,7 +491,8 @@ test('embeds node pagination query as a separate document', async function () {
491
491
  endCursor
492
492
  }
493
493
  }
494
- }\`,
494
+ }
495
+ \`,
495
496
 
496
497
  rootType: "Query",
497
498
 
@@ -640,7 +641,7 @@ test('embeds custom pagination query as a separate document', async function ()
640
641
  export default {
641
642
  name: "UserGhost_Pagination_Query",
642
643
  kind: "HoudiniQuery",
643
- hash: "9fe4fdcb7b5688f1c817afcf77a1ab683ff4f627a787c0f8530ef63b24a19d97",
644
+ hash: "55c27b299d485bf73adfaa418b77ac03d918e2ce579730d328208318c6af0da5",
644
645
 
645
646
  refetch: {
646
647
  update: "append",
@@ -683,7 +684,8 @@ test('embeds custom pagination query as a separate document', async function ()
683
684
  endCursor
684
685
  }
685
686
  }
686
- }\`,
687
+ }
688
+ \`,
687
689
 
688
690
  rootType: "Query",
689
691
 
@@ -1283,7 +1285,7 @@ test('generated query has same refetch spec', async function () {
1283
1285
  export default {
1284
1286
  name: "UserFriends_Pagination_Query",
1285
1287
  kind: "HoudiniQuery",
1286
- hash: "d4e23bbfa5ec61d6682d9e6a99ba50b97300b29f343fb28060a4c0669bf882eb",
1288
+ hash: "5aeb471edf15c5b3e709ddccc6014f073d2dfdc1259d04b7ee26887ea81ef23b",
1287
1289
 
1288
1290
  refetch: {
1289
1291
  update: "append",
@@ -1321,7 +1323,8 @@ test('generated query has same refetch spec', async function () {
1321
1323
  endCursor
1322
1324
  }
1323
1325
  }
1324
- }\`,
1326
+ }
1327
+ \`,
1325
1328
 
1326
1329
  rootType: "Query",
1327
1330
 
@@ -400,7 +400,7 @@ export default async function paginate(
400
400
  kind: graphql.Kind.NAME,
401
401
  value: refetchQueryName,
402
402
  },
403
- operation: graphql.OperationTypeNode.QUERY,
403
+ operation: 'query',
404
404
  variableDefinitions: paginationArgs
405
405
  .map(
406
406
  (arg) =>
@@ -637,7 +637,7 @@ function objectNode([type, defaultValue]: [
637
637
  number | string | undefined
638
638
  ]): graphql.ObjectValueNode {
639
639
  const node = {
640
- kind: graphql.Kind.OBJECT as const,
640
+ kind: graphql.Kind.OBJECT,
641
641
  fields: [
642
642
  {
643
643
  kind: graphql.Kind.OBJECT_FIELD,
@@ -2,6 +2,7 @@
2
2
  import cache from '../cache'
3
3
  import type { ConfigFile } from './config'
4
4
  import * as log from './log'
5
+ import { extractFiles } from './networkUtils'
5
6
  import {
6
7
  CachePolicy,
7
8
  DataSource,
@@ -22,6 +23,58 @@ export class HoudiniClient {
22
23
  this.socket = subscriptionHandler
23
24
  }
24
25
 
26
+ handleMultipart(
27
+ params: FetchParams,
28
+ args: Parameters<FetchContext['fetch']>
29
+ ): Parameters<FetchContext['fetch']> | undefined {
30
+ const [url, req] = args
31
+
32
+ // process any files that could be included
33
+ const { clone, files } = extractFiles({
34
+ query: params.text,
35
+ variables: params.variables,
36
+ })
37
+
38
+ const operationJSON = JSON.stringify(clone)
39
+
40
+ // if there are files in the request
41
+ if (files.size) {
42
+ let headers: Record<string, string> = {}
43
+
44
+ // filters `content-type: application/json` if received by client.ts
45
+ if (req?.headers) {
46
+ const filtered = Object.entries(req?.headers).filter(([key, value]) => {
47
+ return !(
48
+ key.toLowerCase() == 'content-type' &&
49
+ value.toLowerCase() == 'application/json'
50
+ )
51
+ })
52
+ headers = Object.fromEntries(filtered)
53
+ }
54
+
55
+ // See the GraphQL multipart request spec:
56
+ // https://github.com/jaydenseric/graphql-multipart-request-spec
57
+ const form = new FormData()
58
+
59
+ form.set('operations', operationJSON)
60
+
61
+ const map: Record<string, Array<string>> = {}
62
+
63
+ let i = 0
64
+ files.forEach((paths) => {
65
+ map[++i] = paths
66
+ })
67
+ form.set('map', JSON.stringify(map))
68
+
69
+ i = 0
70
+ files.forEach((paths, file) => {
71
+ form.set(`${++i}`, file as Blob, (file as File).name)
72
+ })
73
+
74
+ return [url, { ...req, headers, body: form as any }]
75
+ }
76
+ }
77
+
25
78
  async sendRequest<_Data>(
26
79
  ctx: FetchContext,
27
80
  params: FetchParams
@@ -33,7 +86,11 @@ export class HoudiniClient {
33
86
  // wrap the user's fetch function so we can identify SSR by checking
34
87
  // the response.url
35
88
  fetch: async (...args: Parameters<FetchContext['fetch']>) => {
36
- const response = await ctx.fetch(...args)
89
+ // figure out if we need to do something special for multipart uploads
90
+ const newArgs = this.handleMultipart(params, args)
91
+
92
+ // use the new args if they exist, otherwise the old ones are good
93
+ const response = await ctx.fetch(...(newArgs || args))
37
94
  if (response.url) {
38
95
  url = response.url
39
96
  }
@@ -0,0 +1,151 @@
1
+ /// This file contains a modified version, made by AlecAivazis, of the functions found here: https://github.com/jaydenseric/extract-files/blob/master/extractFiles.mjs
2
+ /// The associated license is at the end of the file (per the project's license agreement)
3
+
4
+ export function isExtractableFile(value: any): value is ExtractableFile {
5
+ return (
6
+ (typeof File !== 'undefined' && value instanceof File) ||
7
+ (typeof Blob !== 'undefined' && value instanceof Blob)
8
+ )
9
+ }
10
+
11
+ type ExtractableFile = File | Blob
12
+
13
+ /** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */
14
+
15
+ export function extractFiles(value: any) {
16
+ if (!arguments.length) throw new TypeError('Argument 1 `value` is required.')
17
+
18
+ /**
19
+ * Deeply clonable value.
20
+ * @typedef {Array<unknown> | FileList | Record<PropertyKey, unknown>} Cloneable
21
+ */
22
+
23
+ /**
24
+ * Clone of a {@link Cloneable deeply cloneable value}.
25
+ * @typedef {Exclude<Cloneable, FileList>} Clone
26
+ */
27
+
28
+ /**
29
+ * Map of values recursed within the input value and their clones, for reusing
30
+ * clones of values that are referenced multiple times within the input value.
31
+ * @type {Map<Cloneable, Clone>}
32
+ */
33
+ const clones = new Map()
34
+
35
+ /**
36
+ * Extracted files and their object paths within the input value.
37
+ * @type {Extraction<Extractable>["files"]}
38
+ */
39
+ const files = new Map()
40
+
41
+ /**
42
+ * Recursively clones the value, extracting files.
43
+ */
44
+ function recurse(value: any, path: string | string[], recursed: Set<any>) {
45
+ if (isExtractableFile(value)) {
46
+ const filePaths = files.get(value)
47
+
48
+ filePaths ? filePaths.push(path) : files.set(value, [path])
49
+
50
+ return null
51
+ }
52
+
53
+ const valueIsList =
54
+ Array.isArray(value) || (typeof FileList !== 'undefined' && value instanceof FileList)
55
+ const valueIsPlainObject = isPlainObject(value)
56
+
57
+ if (valueIsList || valueIsPlainObject) {
58
+ let clone = clones.get(value)
59
+
60
+ const uncloned = !clone
61
+
62
+ if (uncloned) {
63
+ clone = valueIsList
64
+ ? []
65
+ : // Replicate if the plain object is an `Object` instance.
66
+ value instanceof /** @type {any} */ Object
67
+ ? {}
68
+ : Object.create(null)
69
+
70
+ clones.set(value, /** @type {Clone} */ clone)
71
+ }
72
+
73
+ if (!recursed.has(value)) {
74
+ const pathPrefix = path ? `${path}.` : ''
75
+ const recursedDeeper = new Set(recursed).add(value)
76
+
77
+ if (valueIsList) {
78
+ let index = 0
79
+
80
+ // @ts-ignore
81
+ for (const item of value) {
82
+ const itemClone = recurse(item, pathPrefix + index++, recursedDeeper)
83
+
84
+ if (uncloned) /** @type {Array<unknown>} */ clone.push(itemClone)
85
+ }
86
+ } else
87
+ for (const key in value) {
88
+ const propertyClone = recurse(value[key], pathPrefix + key, recursedDeeper)
89
+
90
+ if (uncloned)
91
+ /** @type {Record<PropertyKey, unknown>} */ clone[key] = propertyClone
92
+ }
93
+ }
94
+
95
+ return clone
96
+ }
97
+
98
+ return value
99
+ }
100
+
101
+ return {
102
+ clone: recurse(value, '', new Set()),
103
+ files,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * An extraction result.
109
+ * @template [Extractable=unknown] Extractable file type.
110
+ * @typedef {object} Extraction
111
+ * @prop {unknown} clone Clone of the original value with extracted files
112
+ * recursively replaced with `null`.
113
+ * @prop {Map<Extractable, Array<ObjectPath>>} files Extracted files and their
114
+ * object paths within the original value.
115
+ */
116
+
117
+ /**
118
+ * String notation for the path to a node in an object tree.
119
+ * @typedef {string} ObjectPath
120
+ * @see [`object-path` on npm](https://npm.im/object-path).
121
+ * @example
122
+ * An object path for object property `a`, array index `0`, object property `b`:
123
+ *
124
+ * ```
125
+ * a.0.b
126
+ * ```
127
+ */
128
+
129
+ function isPlainObject(value: any) {
130
+ if (typeof value !== 'object' || value === null) {
131
+ return false
132
+ }
133
+
134
+ const prototype = Object.getPrototypeOf(value)
135
+ return (
136
+ (prototype === null ||
137
+ prototype === Object.prototype ||
138
+ Object.getPrototypeOf(prototype) === null) &&
139
+ !(Symbol.toStringTag in value) &&
140
+ !(Symbol.iterator in value)
141
+ )
142
+ }
143
+
144
+ // MIT License
145
+ // Copyright Jayden Seric
146
+
147
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
148
+
149
+ // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
150
+
151
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.