@dsh-workbench/shared 0.3.10 → 1.0.0-beta.0

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 (391) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/api/blocker-table.d.ts +3 -3
  4. package/dist/api/blocker-table.js +47 -23
  5. package/dist/api/dsh.d.ts +19 -1
  6. package/dist/api/host-write.d.ts +0 -15
  7. package/dist/api/host-write.js +1 -35
  8. package/dist/api/workbench-client.js +2 -1
  9. package/dist/components/Activity.css +0 -1
  10. package/dist/components/ApprovalDetail.css +1 -2
  11. package/dist/components/ApprovalDetail.d.ts +1 -1
  12. package/dist/components/ApprovalPlan.css +0 -1
  13. package/dist/components/Card.css +0 -1
  14. package/dist/components/Card.d.ts +1 -1
  15. package/dist/components/CardBoundary.d.ts +1 -1
  16. package/dist/components/CardHead.css +0 -1
  17. package/dist/components/CardHead.d.ts +1 -1
  18. package/dist/components/ChoiceGroup.css +1 -2
  19. package/dist/components/ConfirmBar.d.ts +1 -1
  20. package/dist/components/ConfirmBar.js +1 -1
  21. package/dist/components/Form.css +1 -1
  22. package/dist/components/FormActions.js +1 -1
  23. package/dist/components/LongTaskRow.css +1 -2
  24. package/dist/components/LongTaskRow.d.ts +1 -1
  25. package/dist/components/LongTaskRow.js +1 -1
  26. package/dist/components/Note.css +0 -1
  27. package/dist/components/ResourceStates.css +0 -1
  28. package/dist/components/SettingsSection.css +0 -1
  29. package/dist/components/Spinner.css +0 -1
  30. package/dist/components/Stack.css +0 -1
  31. package/dist/components/StateBlock.css +0 -1
  32. package/dist/components/board-module.d.ts +3 -3
  33. package/dist/components/icons.d.ts +1 -7
  34. package/dist/components/icons.js +11 -16
  35. package/dist/config/constants.d.ts +2 -2
  36. package/dist/config/constants.js +2 -2
  37. package/dist/config/storage.d.ts +0 -4
  38. package/dist/config/storage.js +0 -11
  39. package/dist/contract/adapters.d.ts +10 -0
  40. package/dist/contract/cli.d.ts +3 -0
  41. package/dist/contract/data.d.ts +126 -0
  42. package/dist/{api → contract}/facts.d.ts +12 -3
  43. package/dist/{api → contract}/facts.js +19 -1
  44. package/dist/contract/needs.d.ts +132 -0
  45. package/dist/contract/needs.js +232 -0
  46. package/dist/contract/plugin-source.d.ts +1 -0
  47. package/dist/contract/plugin-source.js +1 -0
  48. package/dist/contract/protocol/bundle-install.d.ts +136 -0
  49. package/dist/contract/protocol/bundle-install.js +12 -0
  50. package/dist/contract/protocol/config-sync.d.ts +55 -0
  51. package/dist/contract/protocol/config-sync.js +2 -0
  52. package/dist/contract/protocol/diy.d.ts +4 -0
  53. package/dist/contract/protocol/diy.js +4 -0
  54. package/dist/contract/protocol/dsh-plugins.d.ts +11 -0
  55. package/dist/contract/protocol/dsh-plugins.js +1 -0
  56. package/dist/contract/protocol/long-tasks.d.ts +13 -0
  57. package/dist/contract/protocol/long-tasks.js +1 -0
  58. package/dist/contract/protocol/market-index.d.ts +195 -0
  59. package/dist/contract/protocol/market-index.js +7 -0
  60. package/dist/contract/protocol/platform.d.ts +44 -0
  61. package/dist/contract/protocol/platform.js +1 -0
  62. package/dist/contract/protocol/preflight-wake.d.ts +7 -0
  63. package/dist/contract/protocol/preflight-wake.js +1 -0
  64. package/dist/contract/protocol/profile-create.d.ts +30 -0
  65. package/dist/contract/protocol/profile-create.js +2 -0
  66. package/dist/contract/protocol/sidebar-edition.d.ts +14 -0
  67. package/dist/contract/protocol/sidebar-edition.js +2 -0
  68. package/dist/contract/protocol/unit-manifests.d.ts +6 -0
  69. package/dist/contract/protocol/unit-manifests.js +1 -0
  70. package/dist/contract/unit-board.d.ts +18 -0
  71. package/dist/contract/unit-board.js +1 -0
  72. package/dist/contract/unit-capability.d.ts +34 -0
  73. package/dist/contract/unit-capability.js +1 -0
  74. package/dist/{api → contract}/unit-declaration.d.ts +7 -5
  75. package/dist/{api → contract}/unit-declaration.js +19 -13
  76. package/dist/contract/unit.d.ts +57 -0
  77. package/dist/contract/unit.js +24 -0
  78. package/dist/contract/workbench-contract.d.ts +15 -0
  79. package/dist/contract/workbench-contract.js +1 -0
  80. package/dist/host/adapter-set.d.ts +26 -0
  81. package/dist/host/adapter-set.js +243 -0
  82. package/dist/host/adapter-types.d.ts +79 -0
  83. package/dist/host/adapter-types.js +23 -0
  84. package/dist/host/adapters.d.ts +22 -0
  85. package/dist/host/adapters.js +9 -0
  86. package/dist/host/browser.d.ts +3 -0
  87. package/dist/host/browser.js +3 -0
  88. package/dist/host/changelog.d.ts +21 -0
  89. package/dist/host/changelog.js +188 -0
  90. package/dist/host/channel-plugin.d.ts +15 -0
  91. package/dist/host/channel-plugin.js +42 -0
  92. package/dist/host/cli.d.ts +4 -0
  93. package/dist/host/cli.js +7 -0
  94. package/dist/host/command-name.d.ts +1 -0
  95. package/dist/host/command-name.js +14 -0
  96. package/dist/host/data-error.d.ts +13 -0
  97. package/dist/host/data-error.js +20 -0
  98. package/dist/host/data-root.js +3 -0
  99. package/dist/host/diy-debug.js +2 -1
  100. package/dist/host/diy-record.js +2 -1
  101. package/dist/host/facts.d.ts +1 -1
  102. package/dist/host/facts.js +3 -2
  103. package/dist/host/git.js +2 -2
  104. package/dist/host/http.js +1 -1
  105. package/dist/host/lib-capabilities.d.ts +22 -0
  106. package/dist/host/lib-capabilities.js +37 -0
  107. package/dist/host/lib-install.d.ts +9 -0
  108. package/dist/host/lib-install.js +10 -0
  109. package/dist/host/long-task.d.ts +1 -1
  110. package/dist/host/long-task.js +4 -3
  111. package/dist/host/market-index.d.ts +22 -0
  112. package/dist/host/market-index.js +420 -0
  113. package/dist/host/needs.d.ts +4 -0
  114. package/dist/host/needs.js +2 -0
  115. package/dist/host/patch.d.ts +9 -0
  116. package/dist/host/patch.js +78 -0
  117. package/dist/host/paths.d.ts +12 -0
  118. package/dist/host/paths.js +28 -0
  119. package/dist/host/platform.d.ts +14 -0
  120. package/dist/host/platform.js +40 -0
  121. package/dist/host/plugin-source.d.ts +8 -6
  122. package/dist/host/plugin-source.js +43 -33
  123. package/dist/host/plugin.d.ts +4 -0
  124. package/dist/host/plugin.js +39 -0
  125. package/dist/host/port-types.d.ts +191 -0
  126. package/dist/host/port-types.js +1 -0
  127. package/dist/host/preflight.d.ts +28 -0
  128. package/dist/host/preflight.js +1 -0
  129. package/dist/host/profile-port.d.ts +4 -0
  130. package/dist/host/profile-port.js +4 -0
  131. package/dist/host/provision.d.ts +2 -2
  132. package/dist/host/provision.js +5 -3
  133. package/dist/host/recipe-rules.d.ts +5 -0
  134. package/dist/host/recipe-rules.js +382 -0
  135. package/dist/host/recipe.d.ts +14 -0
  136. package/dist/host/recipe.js +283 -0
  137. package/dist/host/shell-text.d.ts +9 -2
  138. package/dist/host/shell-text.js +15 -3
  139. package/dist/host/shell.d.ts +2 -0
  140. package/dist/host/shell.js +7 -0
  141. package/dist/host/spec.d.ts +8 -0
  142. package/dist/host/spec.js +24 -0
  143. package/dist/host/supply-index.d.ts +34 -0
  144. package/dist/host/supply-index.js +202 -0
  145. package/dist/host/supply-port.d.ts +5 -0
  146. package/dist/host/supply-port.js +39 -0
  147. package/dist/host/time.d.ts +12 -0
  148. package/dist/host/time.js +19 -0
  149. package/dist/host/tokens.d.ts +26 -0
  150. package/dist/host/tokens.js +154 -0
  151. package/dist/host/tool-approval.d.ts +12 -0
  152. package/dist/host/tool-approval.js +4 -0
  153. package/dist/host/tool-kit.js +1 -1
  154. package/dist/host/unit-face.d.ts +5 -4
  155. package/dist/host/version-range.d.ts +2 -0
  156. package/dist/host/version-range.js +73 -0
  157. package/dist/host/version.d.ts +3 -0
  158. package/dist/host/version.js +29 -0
  159. package/dist/host.d.ts +47 -16
  160. package/dist/host.js +47 -16
  161. package/dist/i18n/en.js +259 -86
  162. package/dist/i18n/index.d.ts +333 -88
  163. package/dist/i18n/merge.js +1 -1
  164. package/dist/i18n/need-text.d.ts +26 -0
  165. package/dist/i18n/need-text.js +26 -0
  166. package/dist/i18n/settings.d.ts +70 -0
  167. package/dist/i18n/settings.js +136 -0
  168. package/dist/i18n/zh.d.ts +332 -87
  169. package/dist/i18n/zh.js +260 -87
  170. package/dist/index.d.ts +24 -8
  171. package/dist/index.js +24 -8
  172. package/dist/lib/blocker-action.d.ts +0 -15
  173. package/dist/lib/blocker-action.js +1 -25
  174. package/dist/{api → lib}/dsh-client.d.ts +12 -1
  175. package/dist/lib/dsh-client.js +1 -0
  176. package/dist/lib/market-index.d.ts +7 -0
  177. package/dist/lib/market-index.js +25 -0
  178. package/dist/lib/react-types.d.ts +5 -0
  179. package/dist/lib/react-types.js +1 -0
  180. package/dist/lib/react.d.ts +1 -40
  181. package/dist/lib/react.js +0 -36
  182. package/dist/lib/ui.js +3 -9
  183. package/dist/testing/data.d.ts +13 -0
  184. package/dist/testing/data.js +46 -0
  185. package/dist/testing/declaration.d.ts +1 -1
  186. package/dist/testing/declaration.js +1 -1
  187. package/dist/testing/host-fakes.d.ts +0 -13
  188. package/dist/testing/host-fakes.js +0 -24
  189. package/dist/testing/needs.d.ts +16 -0
  190. package/dist/testing/needs.js +46 -0
  191. package/dist/testing.d.ts +2 -0
  192. package/dist/testing.js +2 -0
  193. package/package.json +18 -2
  194. package/src/api/__tests__/blocker-table.test.ts +135 -86
  195. package/src/api/blocker-table.ts +52 -26
  196. package/src/api/dsh.ts +17 -1
  197. package/src/api/host-write.ts +0 -43
  198. package/src/api/workbench-client.test.ts +2 -2
  199. package/src/api/workbench-client.ts +2 -1
  200. package/src/components/Activity.css +0 -1
  201. package/src/components/ApprovalDetail.css +1 -2
  202. package/src/components/ApprovalDetail.tsx +1 -1
  203. package/src/components/ApprovalPlan.css +0 -1
  204. package/src/components/Card.css +0 -1
  205. package/src/components/Card.tsx +1 -1
  206. package/src/components/CardAnchor.test.tsx +1 -1
  207. package/src/components/CardBoundary.tsx +1 -1
  208. package/src/components/CardHead.css +0 -1
  209. package/src/components/CardHead.tsx +1 -1
  210. package/src/components/ChoiceGroup.css +1 -2
  211. package/src/components/ConfirmBar.tsx +1 -1
  212. package/src/components/Form.css +1 -1
  213. package/src/components/Form.tsx +1 -1
  214. package/src/components/FormActions.tsx +1 -1
  215. package/src/components/LongTaskRow.css +1 -2
  216. package/src/components/LongTaskRow.tsx +1 -1
  217. package/src/components/Note.css +0 -1
  218. package/src/components/ResourceStates.css +0 -1
  219. package/src/components/SelectFieldNodes.tsx +0 -1
  220. package/src/components/SettingsSection.css +0 -1
  221. package/src/components/Spinner.css +0 -1
  222. package/src/components/Stack.css +0 -1
  223. package/src/components/StateBlock.css +0 -1
  224. package/src/components/__tests__/long-task-row.test.ts +1 -1
  225. package/src/components/board-module.ts +3 -3
  226. package/src/components/icons.tsx +16 -24
  227. package/src/config/constants.ts +2 -2
  228. package/src/config/storage.test.ts +1 -25
  229. package/src/config/storage.ts +0 -12
  230. package/src/{api → contract}/__tests__/facts.test.ts +73 -25
  231. package/src/{api → contract}/__tests__/long-task.test.ts +3 -3
  232. package/src/contract/__tests__/needs.test.ts +74 -0
  233. package/src/{host → contract}/__tests__/unit-declaration.test.ts +1 -1
  234. package/src/contract/__tests__/unit.test.ts +43 -0
  235. package/src/contract/adapters.ts +9 -0
  236. package/src/contract/cli.ts +3 -0
  237. package/src/contract/data.ts +145 -0
  238. package/src/{api → contract}/facts.ts +34 -4
  239. package/src/contract/needs.ts +331 -0
  240. package/src/contract/plugin-source.ts +1 -0
  241. package/src/contract/protocol/bundle-install.ts +198 -0
  242. package/src/contract/protocol/config-sync.ts +65 -0
  243. package/src/contract/protocol/diy.ts +4 -0
  244. package/src/contract/protocol/dsh-plugins.ts +13 -0
  245. package/src/contract/protocol/long-tasks.ts +16 -0
  246. package/src/contract/protocol/market-index.ts +246 -0
  247. package/src/contract/protocol/platform.ts +47 -0
  248. package/src/contract/protocol/preflight-wake.ts +17 -0
  249. package/src/contract/protocol/profile-create.ts +36 -0
  250. package/src/contract/protocol/sidebar-edition.ts +19 -0
  251. package/src/contract/protocol/unit-manifests.ts +8 -0
  252. package/src/contract/unit-board.ts +23 -0
  253. package/src/contract/unit-capability.ts +37 -0
  254. package/src/{host → contract}/unit-declaration.ts +22 -18
  255. package/src/contract/unit.ts +76 -0
  256. package/src/contract/workbench-contract.ts +30 -0
  257. package/src/host/__tests__/adapter-set.test.ts +228 -0
  258. package/src/host/__tests__/adapters.test.ts +27 -0
  259. package/src/host/__tests__/changelog.test.ts +102 -0
  260. package/src/host/__tests__/command-name.test.ts +16 -0
  261. package/src/host/__tests__/diy-record.test.ts +3 -3
  262. package/src/host/__tests__/facts.test.ts +5 -5
  263. package/src/host/__tests__/long-task.test.ts +1 -1
  264. package/src/host/__tests__/market-index.test.ts +179 -0
  265. package/src/host/__tests__/plugin-ledger.test.ts +10 -10
  266. package/src/host/__tests__/provision.test.ts +7 -6
  267. package/src/host/__tests__/shell-text.test.ts +16 -1
  268. package/src/host/__tests__/supply-index.test.ts +137 -0
  269. package/src/host/__tests__/tokens.test.ts +110 -0
  270. package/src/host/__tests__/version-range.test.ts +21 -0
  271. package/src/host/adapter-set.ts +298 -0
  272. package/src/host/adapter-types.ts +105 -0
  273. package/src/host/adapters.ts +44 -0
  274. package/src/host/browser.ts +3 -0
  275. package/src/host/changelog.ts +212 -0
  276. package/src/host/channel-plugin.ts +55 -0
  277. package/src/host/cli.ts +11 -0
  278. package/src/host/command-name.ts +15 -0
  279. package/src/host/data-error.ts +21 -0
  280. package/src/host/data-root.ts +2 -0
  281. package/src/host/diy-debug.ts +2 -1
  282. package/src/host/diy-record.ts +2 -1
  283. package/src/host/facts.ts +4 -3
  284. package/src/host/git.ts +2 -2
  285. package/src/host/http.ts +1 -1
  286. package/src/host/lib-capabilities.ts +52 -0
  287. package/src/host/lib-install.ts +18 -0
  288. package/src/host/long-task.ts +4 -3
  289. package/src/host/market-index.ts +413 -0
  290. package/src/host/needs.ts +6 -0
  291. package/src/host/patch.ts +93 -0
  292. package/src/host/paths.ts +37 -0
  293. package/src/host/platform.ts +48 -0
  294. package/src/host/plugin-source.ts +48 -56
  295. package/src/host/plugin.ts +39 -0
  296. package/src/host/port-types.ts +222 -0
  297. package/src/host/preflight.ts +31 -0
  298. package/src/host/profile-port.ts +9 -0
  299. package/src/host/provision.ts +7 -4
  300. package/src/host/recipe-rules.ts +330 -0
  301. package/src/host/recipe.test.ts +334 -0
  302. package/src/host/recipe.ts +323 -0
  303. package/src/host/shell-text.ts +15 -3
  304. package/src/host/shell.ts +9 -0
  305. package/src/host/spec.ts +32 -0
  306. package/src/host/supply-index.ts +256 -0
  307. package/src/host/supply-port.ts +45 -0
  308. package/src/host/time.ts +22 -0
  309. package/src/host/tokens.ts +186 -0
  310. package/src/host/tool-approval.ts +17 -0
  311. package/src/host/tool-kit.ts +1 -1
  312. package/src/host/unit-face.ts +5 -4
  313. package/src/host/version-range.ts +71 -0
  314. package/src/host/version.ts +30 -0
  315. package/src/host.ts +47 -16
  316. package/src/i18n/en.ts +261 -86
  317. package/src/i18n/merge.test.ts +6 -6
  318. package/src/i18n/merge.ts +1 -1
  319. package/src/i18n/need-text.ts +27 -0
  320. package/src/i18n/settings.ts +141 -0
  321. package/src/i18n/zh.ts +263 -87
  322. package/src/index.ts +24 -8
  323. package/src/lib/blocker-action.ts +0 -28
  324. package/src/{api → lib}/dsh-client.ts +16 -2
  325. package/src/lib/market-index.ts +36 -0
  326. package/src/lib/react-types.ts +5 -0
  327. package/src/lib/react.ts +1 -40
  328. package/src/lib/text.test.ts +2 -2
  329. package/src/lib/ui.test.ts +6 -5
  330. package/src/lib/ui.ts +3 -9
  331. package/src/testing/__tests__/declaration.test.ts +1 -1
  332. package/src/testing/data.ts +66 -0
  333. package/src/testing/declaration.ts +2 -2
  334. package/src/testing/host-fakes.ts +0 -38
  335. package/src/testing/needs.ts +80 -0
  336. package/src/testing.ts +2 -0
  337. package/dist/api/unit-board.d.ts +0 -39
  338. package/dist/api/unit-board.js +0 -45
  339. package/dist/api/unit.d.ts +0 -85
  340. package/dist/api/unit.js +0 -8
  341. package/dist/api/workbench-contract.d.ts +0 -44
  342. package/dist/components/MockStrip.css +0 -39
  343. package/dist/components/MockStrip.d.ts +0 -15
  344. package/dist/components/MockStrip.js +0 -8
  345. package/dist/components/SaveBar.css +0 -7
  346. package/dist/host/browser-find.d.ts +0 -38
  347. package/dist/host/browser-find.js +0 -242
  348. package/dist/host/browser-read.d.ts +0 -28
  349. package/dist/host/browser-read.js +0 -243
  350. package/dist/host/opencli-runtime.d.ts +0 -25
  351. package/dist/host/opencli-runtime.js +0 -56
  352. package/dist/host/opencli-window.d.ts +0 -2
  353. package/dist/host/opencli-window.js +0 -8
  354. package/dist/host/opencli.d.ts +0 -37
  355. package/dist/host/opencli.js +0 -155
  356. package/dist/host/unit-declaration.d.ts +0 -65
  357. package/dist/host/unit-declaration.js +0 -213
  358. package/dist/host/unit-libs.d.ts +0 -37
  359. package/dist/lib/diy-debug.d.ts +0 -80
  360. package/dist/lib/diy-debug.js +0 -88
  361. package/dist/lib/diy-record.d.ts +0 -135
  362. package/dist/lib/diy-record.js +0 -162
  363. package/dist/lib/lint-probe.d.ts +0 -1
  364. package/dist/lib/lint-probe.js +0 -6
  365. package/dist/lib/panel-state.d.ts +0 -120
  366. package/dist/lib/panel-state.js +0 -176
  367. package/src/api/__tests__/host-write.test.ts +0 -75
  368. package/src/api/__tests__/unit-board.test.ts +0 -76
  369. package/src/api/__tests__/unit.test.ts +0 -38
  370. package/src/api/unit-board.ts +0 -69
  371. package/src/api/unit.ts +0 -93
  372. package/src/api/workbench-contract.ts +0 -59
  373. package/src/host/__tests__/browser-find.test.ts +0 -330
  374. package/src/host/__tests__/browser-read.test.ts +0 -228
  375. package/src/host/__tests__/opencli-runtime.test.ts +0 -46
  376. package/src/host/browser-find.ts +0 -311
  377. package/src/host/browser-read.ts +0 -311
  378. package/src/host/opencli-runtime.ts +0 -79
  379. package/src/host/opencli-window.ts +0 -11
  380. package/src/host/opencli.ts +0 -213
  381. package/src/host/unit-libs.ts +0 -41
  382. package/src/lib/blocker-action.test.ts +0 -40
  383. /package/dist/{api/blocker.js → contract/adapters.js} +0 -0
  384. /package/dist/{api → contract}/blocker.d.ts +0 -0
  385. /package/dist/{api/dsh-client.js → contract/blocker.js} +0 -0
  386. /package/dist/{api/workbench-contract.js → contract/cli.js} +0 -0
  387. /package/dist/{host/unit-libs.js → contract/data.js} +0 -0
  388. /package/dist/{api → contract}/long-task.d.ts +0 -0
  389. /package/dist/{api → contract}/long-task.js +0 -0
  390. /package/src/{api → contract}/blocker.ts +0 -0
  391. /package/src/{api → contract}/long-task.ts +0 -0
@@ -0,0 +1,382 @@
1
+ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
2
+ const RESPONSE_KINDS = ['json', 'text'];
3
+ const CONTENT_TYPES = ['json', 'form'];
4
+ const CREDENTIAL_HEADERS = [
5
+ 'authorization',
6
+ 'cookie',
7
+ 'x-api-key',
8
+ 'x-auth-token',
9
+ 'x-csrf-token',
10
+ 'x-xsrf-token',
11
+ ];
12
+ export const DEFAULT_RECIPE_MAX_BYTES = 2 * 1024 * 1024;
13
+ export const MAX_RECIPE_CALLS = 8;
14
+ const MAX_MAX_BYTES = 8 * 1024 * 1024;
15
+ function recordOf(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
17
+ ? value
18
+ : undefined;
19
+ }
20
+ function textOf(value) {
21
+ return typeof value === 'string' ? value.trim() : '';
22
+ }
23
+ function httpOriginProblem(value) {
24
+ const text = textOf(value);
25
+ if (text === '')
26
+ return 'origin 不能为空';
27
+ try {
28
+ const url = new URL(text);
29
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
30
+ return 'origin 只认 http(s)';
31
+ if (url.origin !== text)
32
+ return 'origin 只写到域(不带路径、不带尾斜杠)';
33
+ }
34
+ catch {
35
+ return `origin 不是合法地址:${text}`;
36
+ }
37
+ return undefined;
38
+ }
39
+ function relativePathProblem(value, label) {
40
+ const text = textOf(value);
41
+ if (text === '')
42
+ return `${label} 不能为空`;
43
+ if (text.includes('\\'))
44
+ return `${label} 里不许有反斜杠`;
45
+ if (text.startsWith('//'))
46
+ return `${label} 不许用协议相对地址`;
47
+ if (text.includes('?') || text.includes('#'))
48
+ return `${label} 不带查询 / 锚点,参数走 query`;
49
+ if (!text.startsWith('/'))
50
+ return `${label} 要以 / 开头`;
51
+ return undefined;
52
+ }
53
+ function absoluteUrlProblem(value, label) {
54
+ const text = textOf(value);
55
+ if (text === '')
56
+ return `${label} 不能为空`;
57
+ try {
58
+ const url = new URL(text);
59
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
60
+ return `${label} 只认 http(s)`;
61
+ }
62
+ catch {
63
+ return `${label} 不是合法地址:${text}`;
64
+ }
65
+ return undefined;
66
+ }
67
+ function requestPathProblem(value, origin, label) {
68
+ const text = textOf(value);
69
+ const relative = relativePathProblem(value, label);
70
+ if (relative === undefined)
71
+ return undefined;
72
+ if (text.startsWith('/') || text.startsWith('//') || text.includes('\\') || text.includes('?') || text.includes('#')) {
73
+ return relative;
74
+ }
75
+ try {
76
+ const url = new URL(text);
77
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
78
+ return `${label} 只认 http(s)`;
79
+ if (url.origin !== origin)
80
+ return `${label} 要和 origin 同源`;
81
+ }
82
+ catch {
83
+ return `${label} 要以 / 开头,或者是同源的绝对地址`;
84
+ }
85
+ return undefined;
86
+ }
87
+ function resolvedUrlProblem(value, origin, label) {
88
+ const text = textOf(value);
89
+ if (text === '')
90
+ return `${label} 不能为空`;
91
+ if (text.startsWith('//'))
92
+ return `${label} 不许用协议相对地址`;
93
+ return absoluteUrlProblem(text.startsWith('/') ? `${origin}${text}` : text, label);
94
+ }
95
+ function queryProblem(value) {
96
+ if (value === undefined)
97
+ return undefined;
98
+ const map = recordOf(value);
99
+ if (map === undefined)
100
+ return 'query 要是对象';
101
+ for (const [name, entry] of Object.entries(map)) {
102
+ if (name.trim() === '')
103
+ return 'query 的键不能为空';
104
+ if (typeof entry !== 'string' && typeof entry !== 'number' && typeof entry !== 'boolean') {
105
+ return `query.${name} 只能是字符串 / 数字 / 布尔`;
106
+ }
107
+ }
108
+ return undefined;
109
+ }
110
+ function headersProblem(value) {
111
+ if (value === undefined)
112
+ return undefined;
113
+ const map = recordOf(value);
114
+ if (map === undefined)
115
+ return 'headers 要是对象';
116
+ for (const [name, entry] of Object.entries(map)) {
117
+ if (name.trim() === '')
118
+ return 'headers 的键不能为空';
119
+ if (typeof entry !== 'string')
120
+ return `headers.${name} 要是字符串`;
121
+ if (CREDENTIAL_HEADERS.includes(name.toLowerCase())) {
122
+ return `headers 里不许写凭据头 ${name} —— 凭据走 auth.place`;
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+ function bodyProblem(value, contentType) {
128
+ if (value === undefined)
129
+ return undefined;
130
+ if (contentType === 'form') {
131
+ const map = recordOf(value);
132
+ if (map === undefined)
133
+ return 'contentType: form 时 body 要是对象';
134
+ for (const [name, entry] of Object.entries(map)) {
135
+ if (typeof entry !== 'string')
136
+ return `contentType: form 时 body.${name} 要是字符串`;
137
+ }
138
+ return undefined;
139
+ }
140
+ try {
141
+ JSON.stringify(value);
142
+ }
143
+ catch {
144
+ return 'body 要能 JSON 序列化';
145
+ }
146
+ return undefined;
147
+ }
148
+ function sourceProblem(value, origin) {
149
+ const source = recordOf(value);
150
+ if (source === undefined)
151
+ return 'auth.source 要是对象';
152
+ const kind = textOf(source['kind']);
153
+ if (kind === 'cookie')
154
+ return undefined;
155
+ if (kind === 'cookieValue') {
156
+ return textOf(source['name']) === '' ? 'auth.source.name 不能为空' : undefined;
157
+ }
158
+ if (kind === 'meta') {
159
+ return textOf(source['name']) === '' ? 'auth.source.name 不能为空' : undefined;
160
+ }
161
+ if (kind === 'global') {
162
+ return textOf(source['path']) === '' ? 'auth.source.path 不能为空' : undefined;
163
+ }
164
+ if (kind === 'storage') {
165
+ if (textOf(source['area']) !== 'local' && textOf(source['area']) !== 'session') {
166
+ return 'auth.source.area 只认 local / session';
167
+ }
168
+ return textOf(source['key']) === '' ? 'auth.source.key 不能为空' : undefined;
169
+ }
170
+ if (kind === 'endpoint') {
171
+ const urlIssue = resolvedUrlProblem(source['url'], origin, 'auth.source.url');
172
+ if (urlIssue !== undefined)
173
+ return urlIssue;
174
+ const method = textOf(source['method']);
175
+ if (method !== '' && method !== 'GET' && method !== 'POST') {
176
+ return 'auth.source.method 只认 GET / POST';
177
+ }
178
+ if (textOf(source['path']) === '')
179
+ return 'auth.source.path(token 在返回里的位置)不能为空';
180
+ return bodyProblem(source['body'], 'json');
181
+ }
182
+ return `不认识的 auth.source.kind:${kind === '' ? '(空)' : kind}`;
183
+ }
184
+ function placeProblem(value, source) {
185
+ const place = recordOf(value);
186
+ if (place === undefined)
187
+ return 'auth.place 要是对象';
188
+ const where = textOf(place['in']);
189
+ if (where === 'cookie') {
190
+ if (source !== undefined && source.kind !== 'cookie') {
191
+ return '凭据不是 cookie,place.in 不能是 cookie';
192
+ }
193
+ return undefined;
194
+ }
195
+ if (where === 'header' || where === 'query') {
196
+ if (textOf(place['name']) === '')
197
+ return 'auth.place.name 不能为空';
198
+ if (source !== undefined && source.kind === 'cookie') {
199
+ return 'cookie 来源没有 token 可放,place.in 要写 cookie';
200
+ }
201
+ return undefined;
202
+ }
203
+ if (where === 'body') {
204
+ if (textOf(place['field']) === '')
205
+ return 'auth.place.field 不能为空';
206
+ if (source !== undefined && source.kind === 'cookie') {
207
+ return 'cookie 来源没有 token 可放,place.in 要写 cookie';
208
+ }
209
+ return undefined;
210
+ }
211
+ return `不认识的 auth.place.in:${where === '' ? '(空)' : where}`;
212
+ }
213
+ function authProblem(value, origin) {
214
+ if (value === undefined)
215
+ return undefined;
216
+ const auth = recordOf(value);
217
+ if (auth === undefined)
218
+ return 'auth 要是对象';
219
+ if (textOf(auth['kind']) === 'none') {
220
+ if (auth['source'] !== undefined || auth['place'] !== undefined) {
221
+ return 'auth.kind: none 时不许带 source / place';
222
+ }
223
+ return undefined;
224
+ }
225
+ if (auth['kind'] !== undefined)
226
+ return 'auth.kind 只认 none';
227
+ const sourceIssue = sourceProblem(auth['source'], origin);
228
+ if (sourceIssue !== undefined)
229
+ return sourceIssue;
230
+ return placeProblem(auth['place'], auth['source']);
231
+ }
232
+ function maxBytesProblem(value) {
233
+ if (value === undefined)
234
+ return undefined;
235
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0)
236
+ return 'response.maxBytes 要是正整数';
237
+ if (value > MAX_MAX_BYTES)
238
+ return `response.maxBytes 最大 ${MAX_MAX_BYTES}`;
239
+ return undefined;
240
+ }
241
+ function responseProblem(value) {
242
+ if (value === undefined)
243
+ return undefined;
244
+ const response = recordOf(value);
245
+ if (response === undefined)
246
+ return 'response 要是对象';
247
+ const kind = textOf(response['kind']);
248
+ if (!RESPONSE_KINDS.includes(kind))
249
+ return `response.kind 只认 ${RESPONSE_KINDS.join(' / ')}`;
250
+ if (response['path'] !== undefined && textOf(response['path']) === '')
251
+ return 'response.path 不能为空';
252
+ return maxBytesProblem(response['maxBytes']);
253
+ }
254
+ function responseOverrideProblem(value) {
255
+ if (value === undefined)
256
+ return undefined;
257
+ const response = recordOf(value);
258
+ if (response === undefined)
259
+ return 'response 要是对象';
260
+ if (response['kind'] !== undefined && !RESPONSE_KINDS.includes(textOf(response['kind']))) {
261
+ return `response.kind 只认 ${RESPONSE_KINDS.join(' / ')}`;
262
+ }
263
+ if (response['path'] !== undefined && textOf(response['path']) === '')
264
+ return 'response.path 不能为空';
265
+ return maxBytesProblem(response['maxBytes']);
266
+ }
267
+ function authOf(recipe) {
268
+ return recipe.auth ?? { source: { kind: 'cookie' }, place: { in: 'cookie' } };
269
+ }
270
+ function overridesProblem(recipe, overrides) {
271
+ if (overrides.path !== undefined) {
272
+ const pathIssue = requestPathProblem(overrides.path, recipe.origin, 'path');
273
+ if (pathIssue !== undefined)
274
+ return pathIssue;
275
+ }
276
+ const queryIssue = queryProblem(overrides.query);
277
+ if (queryIssue !== undefined)
278
+ return queryIssue;
279
+ const headersIssue = headersProblem(overrides.headers);
280
+ if (headersIssue !== undefined)
281
+ return headersIssue;
282
+ const responseIssue = responseOverrideProblem(overrides.response);
283
+ if (responseIssue !== undefined)
284
+ return responseIssue;
285
+ const contentType = recipe.request.contentType ?? 'json';
286
+ const bodyIssue = bodyProblem(overrides.body, contentType);
287
+ if (bodyIssue !== undefined)
288
+ return bodyIssue;
289
+ if (overrides.body !== undefined && recipe.request.method === 'GET') {
290
+ return 'GET 不带 body';
291
+ }
292
+ const auth = authOf(recipe);
293
+ const place = auth.kind === 'none' ? undefined : auth.place;
294
+ if (place?.in === 'body' && overrides.body !== undefined && recordOf(overrides.body) === undefined) {
295
+ return 'auth.place 是 body 时 overrides.body 要是对象';
296
+ }
297
+ return undefined;
298
+ }
299
+ export function recipeProblemOf(recipe, overrides) {
300
+ const box = recordOf(recipe);
301
+ if (box === undefined)
302
+ return '配方要是对象';
303
+ const originIssue = httpOriginProblem(box['origin']);
304
+ if (originIssue !== undefined)
305
+ return originIssue;
306
+ const origin = textOf(box['origin']);
307
+ if (box['loginUrl'] !== undefined) {
308
+ const loginIssue = absoluteUrlProblem(box['loginUrl'], 'loginUrl');
309
+ if (loginIssue !== undefined)
310
+ return loginIssue;
311
+ }
312
+ if (box['session'] !== undefined && textOf(box['session']) === '')
313
+ return 'session 不能为空';
314
+ if (box['timeoutMs'] !== undefined) {
315
+ const timeout = box['timeoutMs'];
316
+ if (typeof timeout !== 'number' || !Number.isInteger(timeout) || timeout <= 0) {
317
+ return 'timeoutMs 要是正整数';
318
+ }
319
+ }
320
+ const request = recordOf(box['request']);
321
+ if (request === undefined)
322
+ return 'request 要是对象';
323
+ const method = textOf(request['method']);
324
+ if (!METHODS.includes(method))
325
+ return `request.method 只认 ${METHODS.join(' / ')}`;
326
+ const pathIssue = requestPathProblem(request['path'], origin, 'request.path');
327
+ if (pathIssue !== undefined)
328
+ return pathIssue;
329
+ const queryIssue = queryProblem(request['query']);
330
+ if (queryIssue !== undefined)
331
+ return queryIssue;
332
+ const headersIssue = headersProblem(request['headers']);
333
+ if (headersIssue !== undefined)
334
+ return headersIssue;
335
+ const contentType = textOf(request['contentType']);
336
+ if (contentType !== '' && !CONTENT_TYPES.includes(contentType)) {
337
+ return `request.contentType 只认 ${CONTENT_TYPES.join(' / ')}`;
338
+ }
339
+ const bodyIssue = bodyProblem(request['body'], contentType === 'form' ? 'form' : 'json');
340
+ if (bodyIssue !== undefined)
341
+ return bodyIssue;
342
+ if (request['body'] !== undefined && method === 'GET')
343
+ return 'GET 不带 body';
344
+ const authIssue = authProblem(box['auth'], origin);
345
+ if (authIssue !== undefined)
346
+ return authIssue;
347
+ const auth = recordOf(box['auth']);
348
+ const place = auth === undefined ? undefined : recordOf(auth['place']);
349
+ if (place !== undefined && textOf(place['in']) === 'body' && request['body'] !== undefined) {
350
+ if (recordOf(request['body']) === undefined)
351
+ return 'auth.place 是 body 时 request.body 要是对象';
352
+ }
353
+ const responseIssue = responseProblem(box['response']);
354
+ if (responseIssue !== undefined)
355
+ return responseIssue;
356
+ if (overrides === undefined)
357
+ return undefined;
358
+ return overridesProblem(recipe, overrides);
359
+ }
360
+ export function callsProblemOf(recipe, calls) {
361
+ const recipeIssue = recipeProblemOf(recipe);
362
+ if (recipeIssue !== undefined)
363
+ return recipeIssue;
364
+ const box = recordOf(calls);
365
+ if (box === undefined)
366
+ return 'calls 要是对象,键就是结果名';
367
+ const names = Object.keys(box);
368
+ if (names.length === 0)
369
+ return 'calls 不能为空';
370
+ if (names.length > MAX_RECIPE_CALLS)
371
+ return `一次最多 ${MAX_RECIPE_CALLS} 条 calls`;
372
+ for (const name of names) {
373
+ if (name.trim() === '')
374
+ return 'calls 的键不能为空';
375
+ if (recordOf(box[name]) === undefined)
376
+ return `calls.${name} 要是对象`;
377
+ const issue = overridesProblem(recipe, box[name]);
378
+ if (issue !== undefined)
379
+ return `calls.${name}:${issue}`;
380
+ }
381
+ return undefined;
382
+ }
@@ -0,0 +1,14 @@
1
+ import type { CallOverrides, RecipeCalls, RequestRecipe } from '../contract/data.js';
2
+ export interface RecipeDescription {
3
+ readonly method: string;
4
+ readonly url: string;
5
+ readonly body?: unknown;
6
+ readonly auth?: {
7
+ readonly source: string;
8
+ readonly place: string;
9
+ readonly name?: string;
10
+ } | undefined;
11
+ }
12
+ export declare function scriptOf(recipe: RequestRecipe, overrides?: CallOverrides): string;
13
+ export declare function describeRecipe(recipe: RequestRecipe, overrides?: CallOverrides): RecipeDescription;
14
+ export declare function scriptOfMany(recipe: RequestRecipe, calls: RecipeCalls): string;
@@ -0,0 +1,283 @@
1
+ import { callsProblemOf, DEFAULT_RECIPE_MAX_BYTES, recipeProblemOf } from './recipe-rules.js';
2
+ function authOf(recipe) {
3
+ return recipe.auth ?? { source: { kind: 'cookie' }, place: { in: 'cookie' } };
4
+ }
5
+ function needsToken(auth) {
6
+ return auth.kind !== 'none' && auth.source.kind !== 'cookie';
7
+ }
8
+ function mergedHeaders(recipe, overrides) {
9
+ return { ...(recipe.request.headers ?? {}), ...(overrides.headers ?? {}) };
10
+ }
11
+ function mergedQuery(recipe, overrides) {
12
+ const query = {};
13
+ for (const [name, value] of Object.entries(recipe.request.query ?? {}))
14
+ query[name] = String(value);
15
+ for (const [name, value] of Object.entries(overrides.query ?? {}))
16
+ query[name] = String(value);
17
+ return query;
18
+ }
19
+ function baseUrlOf(recipe, overrides) {
20
+ const path = overrides.path ?? recipe.request.path;
21
+ return path.startsWith('/') ? `${recipe.origin}${path}` : path;
22
+ }
23
+ function urlOf(recipe, overrides) {
24
+ const url = new URL(baseUrlOf(recipe, overrides));
25
+ for (const [name, value] of Object.entries(mergedQuery(recipe, overrides))) {
26
+ url.searchParams.set(name, value);
27
+ }
28
+ return url.toString();
29
+ }
30
+ function responseOf(recipe, overrides) {
31
+ const base = recipe.response;
32
+ const extra = overrides.response ?? {};
33
+ return {
34
+ kind: extra.kind ?? base?.kind ?? 'json',
35
+ path: extra.path ?? base?.path,
36
+ maxBytes: extra.maxBytes ?? base?.maxBytes ?? DEFAULT_RECIPE_MAX_BYTES,
37
+ };
38
+ }
39
+ function bodyOf(recipe, overrides) {
40
+ return overrides.body !== undefined ? overrides.body : recipe.request.body;
41
+ }
42
+ function quote(value) {
43
+ return JSON.stringify(value);
44
+ }
45
+ function literal(value) {
46
+ return JSON.stringify(value);
47
+ }
48
+ function encodedBody(value) {
49
+ return typeof value === 'string' ? literal(value) : `JSON.stringify(${literal(value)})`;
50
+ }
51
+ function tokenLines(recipe, auth) {
52
+ if (!needsToken(auth))
53
+ return [];
54
+ const source = auth.kind === 'none' ? undefined : auth.source;
55
+ if (source === undefined)
56
+ return [];
57
+ if (source.kind === 'cookieValue') {
58
+ return [
59
+ ' const cookie = (name) => {',
60
+ ' const hit = document.cookie.split("; ").find((one) => one.startsWith(name + "="));',
61
+ ' return hit === undefined ? "" : decodeURIComponent(hit.slice(name.length + 1));',
62
+ ' };',
63
+ ` const token = cookie(${quote(source.name)});`,
64
+ ' if (token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 cookie" };',
65
+ ];
66
+ }
67
+ if (source.kind === 'meta') {
68
+ return [
69
+ ` const meta = document.querySelector(${quote(`meta[name="${source.name}"]`)});`,
70
+ ' const token = meta === null ? "" : (meta.getAttribute("content") || "");',
71
+ ' if (token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 meta" };',
72
+ ];
73
+ }
74
+ if (source.kind === 'global') {
75
+ return [
76
+ ` const token = pick(globalThis, ${quote(source.path)});`,
77
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个全局变量" };',
78
+ ];
79
+ }
80
+ if (source.kind === 'storage') {
81
+ const store = source.area === 'local' ? 'localStorage' : 'sessionStorage';
82
+ const path = source.path === undefined ? '' : source.path;
83
+ return [
84
+ ` const raw = ${store}.getItem(${quote(source.key)});`,
85
+ ' let parsed = raw;',
86
+ ' try { parsed = raw === null ? null : JSON.parse(raw); } catch (error) { parsed = raw; }',
87
+ path === ''
88
+ ? ' const token = parsed;'
89
+ : ` const token = pick(parsed, ${quote(path)});`,
90
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 token" };',
91
+ ];
92
+ }
93
+ if (source.kind === 'endpoint') {
94
+ const method = source.method ?? 'GET';
95
+ const hasBody = source.body !== undefined;
96
+ const endpointUrl = source.url.startsWith('/') ? `${recipe.origin}${source.url}` : source.url;
97
+ const lines = [
98
+ ' let pre;',
99
+ ' try {',
100
+ ` pre = await fetch(${quote(endpointUrl)}, { method: ${quote(method)}, credentials: "include", headers: { "content-type": "application/json" }${hasBody ? `, body: ${encodedBody(source.body)}` : ''} });`,
101
+ ' } catch (error) {',
102
+ ' return { ok: false, code: "fetch-failed", message: String(error) };',
103
+ ' }',
104
+ ' if (pre.status === 401 || pre.status === 403) return { ok: false, code: "not-logged-in" };',
105
+ ' const preText = await pre.text();',
106
+ ' if (!pre.ok) return { ok: false, code: "http-" + pre.status, message: preText.slice(0, 200) };',
107
+ ' let preJson;',
108
+ ' try { preJson = JSON.parse(preText); } catch (error) {',
109
+ ' return { ok: false, code: "shape", message: "换登录态时返回的不是 JSON" };',
110
+ ' }',
111
+ ` const token = pick(preJson, ${quote(source.path)});`,
112
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "换登录态时没拿到 token" };',
113
+ ];
114
+ return lines;
115
+ }
116
+ return [];
117
+ }
118
+ function fetchLine(recipe, hasBody) {
119
+ return ` res = await fetch(url, { method: ${quote(recipe.request.method)}, credentials: "include", headers${hasBody ? ', body' : ''} });`;
120
+ }
121
+ export function scriptOf(recipe, overrides = {}) {
122
+ const problem = recipeProblemOf(recipe, overrides);
123
+ if (problem !== undefined)
124
+ throw new Error(problem);
125
+ const auth = authOf(recipe);
126
+ const body = bodyOf(recipe, overrides);
127
+ const contentType = recipe.request.contentType ?? 'json';
128
+ const place = auth.kind === 'none' ? undefined : auth.place;
129
+ const bodyPlace = place?.in === 'body' ? place : undefined;
130
+ const hasBody = body !== undefined || bodyPlace !== undefined;
131
+ const headers = mergedHeaders(recipe, overrides);
132
+ const headerNames = Object.keys(headers).map((name) => name.toLowerCase());
133
+ if (hasBody && !headerNames.includes('content-type')) {
134
+ headers['content-type'] = contentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json';
135
+ }
136
+ const url = urlOf(recipe, overrides);
137
+ const { kind: responseKind, path: responsePath, maxBytes } = responseOf(recipe, overrides);
138
+ const lines = [];
139
+ lines.push('(async () => {');
140
+ lines.push(` if (location.origin !== ${quote(recipe.origin)}) return { ok: false, code: "not-on-site" };`);
141
+ lines.push(' const pick = (value, path) => path.split(".").reduce((one, key) => (one === null || one === undefined ? one : one[key]), value);');
142
+ lines.push(' const set = (target, path, value) => { const parts = path.split("."); let at = target; for (let index = 0; index < parts.length - 1; index += 1) { const key = parts[index]; if (at[key] === null || typeof at[key] !== "object") at[key] = {}; at = at[key]; } at[parts[parts.length - 1]] = value; return target; };');
143
+ lines.push(...tokenLines(recipe, auth));
144
+ lines.push(` const headers = ${literal(headers)};`);
145
+ if (place?.in === 'header' && needsToken(auth)) {
146
+ lines.push(` headers[${quote(place.name)}] = ${quote(place.prefix ?? '')} + token;`);
147
+ }
148
+ if (hasBody) {
149
+ if (contentType === 'form') {
150
+ lines.push(` const body = new URLSearchParams(${literal(body ?? {})});`);
151
+ if (bodyPlace !== undefined)
152
+ lines.push(` body.set(${quote(bodyPlace.field)}, token);`);
153
+ }
154
+ else {
155
+ lines.push(` let body = ${literal(body ?? {})};`);
156
+ if (bodyPlace !== undefined)
157
+ lines.push(` set(body, ${quote(bodyPlace.field)}, token);`);
158
+ lines.push(' if (typeof body !== "string") body = JSON.stringify(body);');
159
+ }
160
+ }
161
+ lines.push(` let url = ${quote(url)};`);
162
+ if (place?.in === 'query' && needsToken(auth)) {
163
+ lines.push(` url += (url.indexOf("?") === -1 ? "?" : "&") + ${quote(`${place.name}=`)} + encodeURIComponent(token);`);
164
+ }
165
+ lines.push(' let res;');
166
+ lines.push(' try {');
167
+ lines.push(fetchLine(recipe, hasBody));
168
+ lines.push(' } catch (error) {');
169
+ lines.push(' return { ok: false, code: "fetch-failed", message: String(error) };');
170
+ lines.push(' }');
171
+ lines.push(' const text = await res.text();');
172
+ lines.push(` if (new TextEncoder().encode(text).length > ${maxBytes}) return { ok: false, code: "too-large", message: "返回超过 ${Math.round(maxBytes / 1024)} KB" };`);
173
+ lines.push(' if (!res.ok) return { ok: false, code: "http-" + res.status, message: text.slice(0, 200) };');
174
+ if (responseKind === 'text') {
175
+ lines.push(' return { ok: true, text };');
176
+ }
177
+ else {
178
+ lines.push(' let data;');
179
+ lines.push(' try {');
180
+ lines.push(' data = JSON.parse(text);');
181
+ lines.push(' } catch (error) {');
182
+ lines.push(' return { ok: false, code: "shape", message: "返回的不是 JSON(可能跳到登录页了)" };');
183
+ lines.push(' }');
184
+ if (responsePath === undefined) {
185
+ lines.push(' return { ok: true, data };');
186
+ }
187
+ else {
188
+ lines.push(` const value = pick(data, ${quote(responsePath)});`);
189
+ lines.push(` if (value === undefined) return { ok: false, code: "shape", message: "返回里没有 ${responsePath} 这一格" };`);
190
+ lines.push(' return { ok: true, data: value };');
191
+ }
192
+ }
193
+ lines.push('})()');
194
+ return lines.join('\n');
195
+ }
196
+ export function describeRecipe(recipe, overrides = {}) {
197
+ const problem = recipeProblemOf(recipe, overrides);
198
+ if (problem !== undefined)
199
+ throw new Error(problem);
200
+ const auth = authOf(recipe);
201
+ const body = bodyOf(recipe, overrides);
202
+ const placement = auth.kind === 'none' ? undefined : auth.place;
203
+ const name = placement === undefined || placement.in === 'cookie'
204
+ ? undefined
205
+ : placement.in === 'body'
206
+ ? placement.field
207
+ : placement.name;
208
+ return {
209
+ method: recipe.request.method,
210
+ url: urlOf(recipe, overrides),
211
+ ...(body === undefined ? {} : { body }),
212
+ ...(placement === undefined
213
+ ? {}
214
+ : {
215
+ auth: {
216
+ source: auth.kind === 'none' ? 'none' : auth.source.kind,
217
+ place: placement.in,
218
+ ...(name === undefined ? {} : { name }),
219
+ },
220
+ }),
221
+ };
222
+ }
223
+ export function scriptOfMany(recipe, calls) {
224
+ const problem = callsProblemOf(recipe, calls);
225
+ if (problem !== undefined)
226
+ throw new Error(problem);
227
+ if (recipe.request.method !== 'GET')
228
+ throw new Error('批量读只认 GET,写一条一条走 submit');
229
+ const auth = authOf(recipe);
230
+ const place = auth.kind === 'none' ? undefined : auth.place;
231
+ if (place?.in === 'body')
232
+ throw new Error('批量读不带 body 里的凭据');
233
+ const lines = [];
234
+ lines.push('(async () => {');
235
+ lines.push(` if (location.origin !== ${quote(recipe.origin)}) return { ok: false, code: "not-on-site" };`);
236
+ lines.push(' const pick = (value, path) => path.split(".").reduce((one, key) => (one === null || one === undefined ? one : one[key]), value);');
237
+ lines.push(...tokenLines(recipe, auth));
238
+ lines.push(` const headers = ${literal(recipe.request.headers ?? {})};`);
239
+ if (place?.in === 'header' && needsToken(auth)) {
240
+ lines.push(` headers[${quote(place.name)}] = ${quote(place.prefix ?? '')} + token;`);
241
+ }
242
+ if (place?.in === 'query' && needsToken(auth)) {
243
+ lines.push(` const withToken = (url) => url + (url.indexOf("?") === -1 ? "?" : "&") + ${quote(`${place.name}=`)} + encodeURIComponent(token);`);
244
+ }
245
+ else {
246
+ lines.push(' const withToken = (url) => url;');
247
+ }
248
+ lines.push(' const one = async (url, headers, kind, path, maxBytes) => {');
249
+ lines.push(' let res;');
250
+ lines.push(' try {');
251
+ lines.push(' res = await fetch(url, { method: "GET", credentials: "include", headers });');
252
+ lines.push(' } catch (error) {');
253
+ lines.push(' return { ok: false, code: "fetch-failed", message: String(error) };');
254
+ lines.push(' }');
255
+ lines.push(' const text = await res.text();');
256
+ lines.push(' if (new TextEncoder().encode(text).length > maxBytes) return { ok: false, code: "too-large", message: "返回超过 " + Math.round(maxBytes / 1024) + " KB" };');
257
+ lines.push(' if (!res.ok) return { ok: false, code: "http-" + res.status, message: text.slice(0, 200) };');
258
+ lines.push(' if (kind === "text") return { ok: true, text };');
259
+ lines.push(' let data;');
260
+ lines.push(' try {');
261
+ lines.push(' data = JSON.parse(text);');
262
+ lines.push(' } catch (error) {');
263
+ lines.push(' return { ok: false, code: "shape", message: "返回的不是 JSON(可能跳到登录页了)" };');
264
+ lines.push(' }');
265
+ lines.push(' if (path === null) return { ok: true, data };');
266
+ lines.push(' const value = pick(data, path);');
267
+ lines.push(' if (value === undefined) return { ok: false, code: "shape", message: "返回里没有 " + path + " 这一格" };');
268
+ lines.push(' return { ok: true, data: value };');
269
+ lines.push(' };');
270
+ lines.push(' const results = {};');
271
+ lines.push(' await Promise.all([');
272
+ for (const [name, call] of Object.entries(calls)) {
273
+ const extra = call.headers ?? {};
274
+ const headersArg = Object.keys(extra).length === 0 ? 'headers' : `Object.assign({}, headers, ${literal(extra)})`;
275
+ const { kind, path, maxBytes } = responseOf(recipe, call);
276
+ const pathArg = path === undefined ? 'null' : quote(path);
277
+ lines.push(` one(withToken(${quote(urlOf(recipe, call))}), ${headersArg}, ${quote(kind)}, ${pathArg}, ${maxBytes}).then((result) => { results[${quote(name)}] = result; }),`);
278
+ }
279
+ lines.push(' ]);');
280
+ lines.push(' return { ok: true, results };');
281
+ lines.push('})()');
282
+ return lines.join('\n');
283
+ }