openapi-typesafe 1.0.0-rc.4 → 1.0.0-rc.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 (320) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/dist/adapters/node-file-system/node-file-system.cjs +7 -0
  3. package/dist/adapters/node-file-system/node-file-system.cjs.map +1 -1
  4. package/dist/adapters/node-file-system/node-file-system.mjs +7 -0
  5. package/dist/adapters/node-file-system/node-file-system.mjs.map +1 -1
  6. package/dist/adapters/pipe-safe-writer/pipe-safe-writer.cjs +19 -0
  7. package/dist/adapters/pipe-safe-writer/pipe-safe-writer.cjs.map +1 -0
  8. package/dist/adapters/pipe-safe-writer/pipe-safe-writer.mjs +17 -0
  9. package/dist/adapters/pipe-safe-writer/pipe-safe-writer.mjs.map +1 -0
  10. package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs +5 -32
  11. package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs.map +1 -1
  12. package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs +6 -33
  13. package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs.map +1 -1
  14. package/dist/adapters/redocly-loader/component-schemas.cjs +17 -0
  15. package/dist/adapters/redocly-loader/component-schemas.cjs.map +1 -0
  16. package/dist/adapters/redocly-loader/component-schemas.mjs +14 -0
  17. package/dist/adapters/redocly-loader/component-schemas.mjs.map +1 -0
  18. package/dist/adapters/redocly-loader/mapping-schema-names.cjs +22 -0
  19. package/dist/adapters/redocly-loader/mapping-schema-names.cjs.map +1 -0
  20. package/dist/adapters/redocly-loader/mapping-schema-names.mjs +20 -0
  21. package/dist/adapters/redocly-loader/mapping-schema-names.mjs.map +1 -0
  22. package/dist/adapters/redocly-loader/query-named-components.cjs +137 -0
  23. package/dist/adapters/redocly-loader/query-named-components.cjs.map +1 -0
  24. package/dist/adapters/redocly-loader/query-named-components.mjs +134 -0
  25. package/dist/adapters/redocly-loader/query-named-components.mjs.map +1 -0
  26. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +26 -14
  27. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
  28. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +26 -14
  29. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
  30. package/dist/adapters/redocly-loader/ref-access.cjs +16 -3
  31. package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
  32. package/dist/adapters/redocly-loader/ref-access.mjs +16 -3
  33. package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
  34. package/dist/adapters/redocly-loader/ref-holders.cjs +52 -10
  35. package/dist/adapters/redocly-loader/ref-holders.cjs.map +1 -1
  36. package/dist/adapters/redocly-loader/ref-holders.mjs +52 -11
  37. package/dist/adapters/redocly-loader/ref-holders.mjs.map +1 -1
  38. package/dist/adapters/redocly-loader/remote-fetch.cjs +5 -3
  39. package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
  40. package/dist/adapters/redocly-loader/remote-fetch.mjs +5 -3
  41. package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
  42. package/dist/adapters/redocly-loader/remote-node-limit.cjs +17 -3
  43. package/dist/adapters/redocly-loader/remote-node-limit.cjs.map +1 -1
  44. package/dist/adapters/redocly-loader/remote-node-limit.mjs +17 -3
  45. package/dist/adapters/redocly-loader/remote-node-limit.mjs.map +1 -1
  46. package/dist/adapters/redocly-loader/schema-source.cjs +7 -2
  47. package/dist/adapters/redocly-loader/schema-source.cjs.map +1 -1
  48. package/dist/adapters/redocly-loader/schema-source.mjs +8 -3
  49. package/dist/adapters/redocly-loader/schema-source.mjs.map +1 -1
  50. package/dist/adapters/terminal-logger/terminal-logger.cjs +7 -3
  51. package/dist/adapters/terminal-logger/terminal-logger.cjs.map +1 -1
  52. package/dist/adapters/terminal-logger/terminal-logger.mjs +7 -3
  53. package/dist/adapters/terminal-logger/terminal-logger.mjs.map +1 -1
  54. package/dist/api/document-refs.cjs +16 -1
  55. package/dist/api/document-refs.cjs.map +1 -1
  56. package/dist/api/document-refs.mjs +16 -1
  57. package/dist/api/document-refs.mjs.map +1 -1
  58. package/dist/api/document-run.cjs +4 -10
  59. package/dist/api/document-run.cjs.map +1 -1
  60. package/dist/api/document-run.mjs +4 -10
  61. package/dist/api/document-run.mjs.map +1 -1
  62. package/dist/api/openapi-ts.cjs +1 -2
  63. package/dist/api/openapi-ts.cjs.map +1 -1
  64. package/dist/api/openapi-ts.mjs +2 -3
  65. package/dist/api/openapi-ts.mjs.map +1 -1
  66. package/dist/api/ref-limit-options.cjs +63 -0
  67. package/dist/api/ref-limit-options.cjs.map +1 -0
  68. package/dist/api/ref-limit-options.mjs +57 -0
  69. package/dist/api/ref-limit-options.mjs.map +1 -0
  70. package/dist/api/transform-schema.cjs +12 -5
  71. package/dist/api/transform-schema.cjs.map +1 -1
  72. package/dist/api/transform-schema.mjs +12 -5
  73. package/dist/api/transform-schema.mjs.map +1 -1
  74. package/dist/application/transform/declared-value-types.cjs +36 -0
  75. package/dist/application/transform/declared-value-types.cjs.map +1 -0
  76. package/dist/application/transform/declared-value-types.mjs +34 -0
  77. package/dist/application/transform/declared-value-types.mjs.map +1 -0
  78. package/dist/application/transform/document/document.cjs +3 -1
  79. package/dist/application/transform/document/document.cjs.map +1 -1
  80. package/dist/application/transform/document/document.mjs +3 -1
  81. package/dist/application/transform/document/document.mjs.map +1 -1
  82. package/dist/application/transform/document/exploded-query-values.cjs +63 -0
  83. package/dist/application/transform/document/exploded-query-values.cjs.map +1 -0
  84. package/dist/application/transform/document/exploded-query-values.mjs +59 -0
  85. package/dist/application/transform/document/exploded-query-values.mjs.map +1 -0
  86. package/dist/application/transform/document/header.cjs +11 -3
  87. package/dist/application/transform/document/header.cjs.map +1 -1
  88. package/dist/application/transform/document/header.mjs +10 -2
  89. package/dist/application/transform/document/header.mjs.map +1 -1
  90. package/dist/application/transform/document/parameter.cjs +1 -1
  91. package/dist/application/transform/document/parameter.cjs.map +1 -1
  92. package/dist/application/transform/document/parameter.mjs +1 -1
  93. package/dist/application/transform/document/parameter.mjs.map +1 -1
  94. package/dist/application/transform/document/parameters.cjs +23 -5
  95. package/dist/application/transform/document/parameters.cjs.map +1 -1
  96. package/dist/application/transform/document/parameters.mjs +23 -5
  97. package/dist/application/transform/document/parameters.mjs.map +1 -1
  98. package/dist/application/transform/document/path-item.cjs +4 -3
  99. package/dist/application/transform/document/path-item.cjs.map +1 -1
  100. package/dist/application/transform/document/path-item.mjs +4 -4
  101. package/dist/application/transform/document/path-item.mjs.map +1 -1
  102. package/dist/application/transform/document/path-template-key.cjs +84 -0
  103. package/dist/application/transform/document/path-template-key.cjs.map +1 -0
  104. package/dist/application/transform/document/path-template-key.mjs +79 -0
  105. package/dist/application/transform/document/path-template-key.mjs.map +1 -0
  106. package/dist/application/transform/document/path-template-refs.cjs +70 -0
  107. package/dist/application/transform/document/path-template-refs.cjs.map +1 -0
  108. package/dist/application/transform/document/path-template-refs.mjs +67 -0
  109. package/dist/application/transform/document/path-template-refs.mjs.map +1 -0
  110. package/dist/application/transform/document/paths-enum.cjs +7 -6
  111. package/dist/application/transform/document/paths-enum.cjs.map +1 -1
  112. package/dist/application/transform/document/paths-enum.mjs +7 -6
  113. package/dist/application/transform/document/paths-enum.mjs.map +1 -1
  114. package/dist/application/transform/document/paths.cjs +38 -66
  115. package/dist/application/transform/document/paths.cjs.map +1 -1
  116. package/dist/application/transform/document/paths.mjs +36 -66
  117. package/dist/application/transform/document/paths.mjs.map +1 -1
  118. package/dist/application/transform/document/webhooks.cjs +10 -3
  119. package/dist/application/transform/document/webhooks.cjs.map +1 -1
  120. package/dist/application/transform/document/webhooks.mjs +9 -2
  121. package/dist/application/transform/document/webhooks.mjs.map +1 -1
  122. package/dist/application/transform/footer/document-footer.cjs +6 -3
  123. package/dist/application/transform/footer/document-footer.cjs.map +1 -1
  124. package/dist/application/transform/footer/document-footer.mjs +6 -3
  125. package/dist/application/transform/footer/document-footer.mjs.map +1 -1
  126. package/dist/application/transform/helper-types.cjs +3 -3
  127. package/dist/application/transform/helper-types.cjs.map +1 -1
  128. package/dist/application/transform/helper-types.mjs +3 -3
  129. package/dist/application/transform/helper-types.mjs.map +1 -1
  130. package/dist/application/transform/ref-pointer/generated-location.cjs +4 -4
  131. package/dist/application/transform/ref-pointer/generated-location.cjs.map +1 -1
  132. package/dist/application/transform/ref-pointer/generated-location.mjs +1 -1
  133. package/dist/application/transform/ref-pointer/generated-location.mjs.map +1 -1
  134. package/dist/application/transform/ref-pointer/generated-shape.cjs +47 -66
  135. package/dist/application/transform/ref-pointer/generated-shape.cjs.map +1 -1
  136. package/dist/application/transform/ref-pointer/generated-shape.mjs +36 -50
  137. package/dist/application/transform/ref-pointer/generated-shape.mjs.map +1 -1
  138. package/dist/application/transform/ref-pointer/inline-cycles.cjs +2 -2
  139. package/dist/application/transform/ref-pointer/inline-cycles.cjs.map +1 -1
  140. package/dist/application/transform/ref-pointer/inline-cycles.mjs +1 -1
  141. package/dist/application/transform/ref-pointer/inline-cycles.mjs.map +1 -1
  142. package/dist/application/transform/ref-pointer/located-node.cjs +23 -0
  143. package/dist/application/transform/ref-pointer/located-node.cjs.map +1 -0
  144. package/dist/application/transform/ref-pointer/located-node.mjs +18 -0
  145. package/dist/application/transform/ref-pointer/located-node.mjs.map +1 -0
  146. package/dist/application/transform/ref-pointer/original-index.cjs +68 -13
  147. package/dist/application/transform/ref-pointer/original-index.cjs.map +1 -1
  148. package/dist/application/transform/ref-pointer/original-index.mjs +70 -15
  149. package/dist/application/transform/ref-pointer/original-index.mjs.map +1 -1
  150. package/dist/application/transform/ref-pointer/pointer-type.cjs +2 -2
  151. package/dist/application/transform/ref-pointer/pointer-type.cjs.map +1 -1
  152. package/dist/application/transform/ref-pointer/pointer-type.mjs +1 -1
  153. package/dist/application/transform/ref-pointer/pointer-type.mjs.map +1 -1
  154. package/dist/application/transform/ref-pointer/pointer-walk.cjs +54 -13
  155. package/dist/application/transform/ref-pointer/pointer-walk.cjs.map +1 -1
  156. package/dist/application/transform/ref-pointer/pointer-walk.mjs +46 -7
  157. package/dist/application/transform/ref-pointer/pointer-walk.mjs.map +1 -1
  158. package/dist/application/transform/ref-pointer/property-shape.cjs +52 -0
  159. package/dist/application/transform/ref-pointer/property-shape.cjs.map +1 -0
  160. package/dist/application/transform/ref-pointer/property-shape.mjs +45 -0
  161. package/dist/application/transform/ref-pointer/property-shape.mjs.map +1 -0
  162. package/dist/application/transform/ref-pointer/typed-walk.cjs +79 -21
  163. package/dist/application/transform/ref-pointer/typed-walk.cjs.map +1 -1
  164. package/dist/application/transform/ref-pointer/typed-walk.mjs +76 -18
  165. package/dist/application/transform/ref-pointer/typed-walk.mjs.map +1 -1
  166. package/dist/application/transform/schema/access-flags.cjs.map +1 -1
  167. package/dist/application/transform/schema/access-flags.mjs.map +1 -1
  168. package/dist/application/transform/schema/index-signature-type.cjs +21 -0
  169. package/dist/application/transform/schema/index-signature-type.cjs.map +1 -0
  170. package/dist/application/transform/schema/index-signature-type.mjs +19 -0
  171. package/dist/application/transform/schema/index-signature-type.mjs.map +1 -0
  172. package/dist/application/transform/schema/input-location.cjs +23 -0
  173. package/dist/application/transform/schema/input-location.cjs.map +1 -0
  174. package/dist/application/transform/schema/input-location.mjs +21 -0
  175. package/dist/application/transform/schema/input-location.mjs.map +1 -0
  176. package/dist/application/transform/schema/object-type.cjs +15 -5
  177. package/dist/application/transform/schema/object-type.cjs.map +1 -1
  178. package/dist/application/transform/schema/object-type.mjs +15 -5
  179. package/dist/application/transform/schema/object-type.mjs.map +1 -1
  180. package/dist/application/transform/schema/schema-transform.cjs +2 -1
  181. package/dist/application/transform/schema/schema-transform.cjs.map +1 -1
  182. package/dist/application/transform/schema/schema-transform.mjs +2 -1
  183. package/dist/application/transform/schema/schema-transform.mjs.map +1 -1
  184. package/dist/application/transform/schema/unevaluated-properties.cjs +2 -30
  185. package/dist/application/transform/schema/unevaluated-properties.cjs.map +1 -1
  186. package/dist/application/transform/schema/unevaluated-properties.mjs +3 -31
  187. package/dist/application/transform/schema/unevaluated-properties.mjs.map +1 -1
  188. package/dist/cli/api-logger.cjs +13 -0
  189. package/dist/cli/api-logger.cjs.map +1 -0
  190. package/dist/cli/api-logger.mjs +11 -0
  191. package/dist/cli/api-logger.mjs.map +1 -0
  192. package/dist/cli/api-settings.cjs +7 -15
  193. package/dist/cli/api-settings.cjs.map +1 -1
  194. package/dist/cli/api-settings.mjs +8 -16
  195. package/dist/cli/api-settings.mjs.map +1 -1
  196. package/dist/cli/cli-arguments.cjs +51 -11
  197. package/dist/cli/cli-arguments.cjs.map +1 -1
  198. package/dist/cli/cli-arguments.mjs +51 -11
  199. package/dist/cli/cli-arguments.mjs.map +1 -1
  200. package/dist/cli/cli-errors.cjs +17 -0
  201. package/dist/cli/cli-errors.cjs.map +1 -1
  202. package/dist/cli/cli-errors.mjs +17 -1
  203. package/dist/cli/cli-errors.mjs.map +1 -1
  204. package/dist/cli/cli-output.cjs +6 -4
  205. package/dist/cli/cli-output.cjs.map +1 -1
  206. package/dist/cli/cli-output.mjs +7 -5
  207. package/dist/cli/cli-output.mjs.map +1 -1
  208. package/dist/cli/main.cjs +10 -4
  209. package/dist/cli/main.cjs.map +1 -1
  210. package/dist/cli/main.mjs +10 -4
  211. package/dist/cli/main.mjs.map +1 -1
  212. package/dist/cli/redocly-apis.cjs +20 -9
  213. package/dist/cli/redocly-apis.cjs.map +1 -1
  214. package/dist/cli/redocly-apis.mjs +22 -11
  215. package/dist/cli/redocly-apis.mjs.map +1 -1
  216. package/dist/cli/ref-limits.cjs +15 -1
  217. package/dist/cli/ref-limits.cjs.map +1 -1
  218. package/dist/cli/ref-limits.mjs +15 -2
  219. package/dist/cli/ref-limits.mjs.map +1 -1
  220. package/dist/cli/run-cli.cjs +24 -7
  221. package/dist/cli/run-cli.cjs.map +1 -1
  222. package/dist/cli/run-cli.mjs +25 -8
  223. package/dist/cli/run-cli.mjs.map +1 -1
  224. package/dist/domain/cause-secrets.cjs +31 -16
  225. package/dist/domain/cause-secrets.cjs.map +1 -1
  226. package/dist/domain/cause-secrets.mjs +31 -16
  227. package/dist/domain/cause-secrets.mjs.map +1 -1
  228. package/dist/domain/errors.cjs +2 -0
  229. package/dist/domain/errors.cjs.map +1 -1
  230. package/dist/domain/errors.mjs +2 -0
  231. package/dist/domain/errors.mjs.map +1 -1
  232. package/dist/domain/location-text.cjs +9 -0
  233. package/dist/domain/location-text.cjs.map +1 -0
  234. package/dist/domain/location-text.mjs +7 -0
  235. package/dist/domain/location-text.mjs.map +1 -0
  236. package/dist/domain/openapi/document-places.cjs +55 -0
  237. package/dist/domain/openapi/document-places.cjs.map +1 -0
  238. package/dist/domain/openapi/document-places.mjs +53 -0
  239. package/dist/domain/openapi/document-places.mjs.map +1 -0
  240. package/dist/domain/output-fixes.cjs +8 -2
  241. package/dist/domain/output-fixes.cjs.map +1 -1
  242. package/dist/domain/output-fixes.mjs +8 -2
  243. package/dist/domain/output-fixes.mjs.map +1 -1
  244. package/dist/domain/terminal-text.cjs +31 -0
  245. package/dist/domain/terminal-text.cjs.map +1 -0
  246. package/dist/domain/terminal-text.mjs +29 -0
  247. package/dist/domain/terminal-text.mjs.map +1 -0
  248. package/dist/domain/warnings.cjs +1 -0
  249. package/dist/domain/warnings.cjs.map +1 -1
  250. package/dist/domain/warnings.mjs +1 -0
  251. package/dist/domain/warnings.mjs.map +1 -1
  252. package/dist/index.d.cts +26 -18
  253. package/dist/index.d.mts +26 -18
  254. package/dist/index.d.ts +26 -18
  255. package/package.json +1 -1
  256. package/src/adapters/node-file-system/node-file-system.ts +14 -1
  257. package/src/adapters/pipe-safe-writer/pipe-safe-writer.ts +29 -0
  258. package/src/adapters/redocly-loader/bundled-mapping-copies.ts +7 -42
  259. package/src/adapters/redocly-loader/component-schemas.ts +19 -0
  260. package/src/adapters/redocly-loader/mapping-schema-names.ts +25 -0
  261. package/src/adapters/redocly-loader/query-named-components.ts +201 -0
  262. package/src/adapters/redocly-loader/redocly-schema-loader.ts +46 -14
  263. package/src/adapters/redocly-loader/ref-access.ts +16 -3
  264. package/src/adapters/redocly-loader/ref-holders.ts +75 -14
  265. package/src/adapters/redocly-loader/remote-fetch.ts +11 -2
  266. package/src/adapters/redocly-loader/remote-node-limit.ts +44 -4
  267. package/src/adapters/redocly-loader/schema-source.ts +15 -3
  268. package/src/adapters/terminal-logger/terminal-logger.ts +10 -4
  269. package/src/api/document-refs.ts +29 -1
  270. package/src/api/document-run.ts +6 -16
  271. package/src/api/openapi-ts.ts +6 -3
  272. package/src/api/ref-limit-options.ts +76 -0
  273. package/src/api/transform-schema.ts +20 -5
  274. package/src/application/ports/file-system.ts +8 -0
  275. package/src/application/transform/declared-value-types.ts +42 -0
  276. package/src/application/transform/document/document.ts +4 -2
  277. package/src/application/transform/document/exploded-query-values.ts +88 -0
  278. package/src/application/transform/document/header.ts +25 -5
  279. package/src/application/transform/document/parameter.ts +4 -4
  280. package/src/application/transform/document/parameters.ts +45 -10
  281. package/src/application/transform/document/path-item.ts +3 -3
  282. package/src/application/transform/document/path-template-key.ts +152 -0
  283. package/src/application/transform/document/path-template-refs.ts +106 -0
  284. package/src/application/transform/document/paths-enum.ts +23 -9
  285. package/src/application/transform/document/paths.ts +53 -123
  286. package/src/application/transform/document/webhooks.ts +18 -5
  287. package/src/application/transform/footer/document-footer.ts +18 -8
  288. package/src/application/transform/helper-types.ts +4 -4
  289. package/src/application/transform/ref-pointer/generated-location.ts +1 -1
  290. package/src/application/transform/ref-pointer/generated-shape.ts +67 -78
  291. package/src/application/transform/ref-pointer/inline-cycles.ts +1 -1
  292. package/src/application/transform/ref-pointer/located-node.ts +38 -0
  293. package/src/application/transform/ref-pointer/original-index.ts +100 -17
  294. package/src/application/transform/ref-pointer/pointer-type.ts +1 -1
  295. package/src/application/transform/ref-pointer/pointer-walk.ts +82 -8
  296. package/src/application/transform/ref-pointer/property-shape.ts +97 -0
  297. package/src/application/transform/ref-pointer/typed-walk.ts +117 -25
  298. package/src/application/transform/schema/access-flags.ts +5 -2
  299. package/src/application/transform/schema/index-signature-type.ts +31 -0
  300. package/src/application/transform/schema/input-location.ts +35 -0
  301. package/src/application/transform/schema/object-type.ts +18 -8
  302. package/src/application/transform/schema/schema-transform.ts +2 -1
  303. package/src/application/transform/schema/unevaluated-properties.ts +4 -44
  304. package/src/cli/api-logger.ts +17 -0
  305. package/src/cli/api-settings.ts +12 -20
  306. package/src/cli/cli-arguments.ts +90 -14
  307. package/src/cli/cli-dependencies.ts +7 -2
  308. package/src/cli/cli-errors.ts +22 -0
  309. package/src/cli/cli-output.ts +11 -10
  310. package/src/cli/main.ts +10 -4
  311. package/src/cli/redocly-apis.ts +37 -17
  312. package/src/cli/ref-limits.ts +21 -2
  313. package/src/cli/run-cli.ts +39 -7
  314. package/src/domain/cause-secrets.ts +59 -25
  315. package/src/domain/errors.ts +2 -0
  316. package/src/domain/location-text.ts +12 -0
  317. package/src/domain/openapi/document-places.ts +78 -0
  318. package/src/domain/output-fixes.ts +9 -2
  319. package/src/domain/terminal-text.ts +44 -0
  320. package/src/domain/warnings.ts +1 -0
@@ -0,0 +1,201 @@
1
+ import {
2
+ appendPointer,
3
+ canonicalRef,
4
+ parseRefPointer,
5
+ ROOT_POINTER,
6
+ } from "../../domain/openapi/json-pointer.js";
7
+ import { findMappings, findRefHolders } from "./ref-holders.js";
8
+
9
+ /**
10
+ * Redocly 1.x bundles an external `$ref` into a component named after the last segment of the
11
+ * document's URL, without its extension, when the pointer gives no free name (a `$ref` to a whole
12
+ * document, or two targets with the same pointer). The query stays in that name, and a query holds
13
+ * API keys and signatures, so `https://host/schemas/pet?token=…` became the component
14
+ * `pet?token=…` in the generated file and in messages (threat model T4b). This renames such a
15
+ * component after the URL without its query: `pet`, with a `-2` suffix when that name is taken.
16
+ */
17
+
18
+ type Components = Record<string, Record<string, unknown>>;
19
+
20
+ /** The keys of each `components` group before bundling: the bundle edits the document in place. */
21
+ export type ComponentKeys = ReadonlyMap<string, ReadonlySet<string>>;
22
+
23
+ /** Old and new name of each renamed component, per `components` group. */
24
+ type Renames = Map<string, Map<string, string>>;
25
+
26
+ const COMPONENTS_KEY = "components";
27
+ const REMOTE_URL_RE = /^https?:\/\//;
28
+ const PATH_SEPARATOR_RE = /[/\\]/;
29
+ const EXTENSION_RE = /\.[^.]+$/;
30
+ /** What Redocly puts between the base name and the rest of a component name. */
31
+ const NAME_SUFFIX_STARTS = ["_", "-"];
32
+ const FIRST_SERIAL = 2;
33
+
34
+ function isContainer(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ function componentsOf(document: unknown): Components {
39
+ const components = isContainer(document) ? document[COMPONENTS_KEY] : undefined;
40
+ if (!isContainer(components)) {
41
+ return {};
42
+ }
43
+ const groups: Components = {};
44
+ for (const [group, entries] of Object.entries(components)) {
45
+ if (isContainer(entries)) {
46
+ groups[group] = entries;
47
+ }
48
+ }
49
+ return groups;
50
+ }
51
+
52
+ /** The keys of every `components` group of a document. */
53
+ export function componentKeysOf(document: unknown): ComponentKeys {
54
+ const keys = new Map<string, ReadonlySet<string>>();
55
+ for (const [group, entries] of Object.entries(componentsOf(document))) {
56
+ keys.set(group, new Set(Object.keys(entries)));
57
+ }
58
+ return keys;
59
+ }
60
+
61
+ /** The base name Redocly 1.34 gives a document (`refBaseName`): the last segment, no extension. */
62
+ function redoclyBaseName(ref: string): string {
63
+ return (ref.split(PATH_SEPARATOR_RE).at(-1) ?? "").replace(EXTENSION_RE, "");
64
+ }
65
+
66
+ /**
67
+ * The base name of the URL without its query and fragment. A URL with no last segment
68
+ * (`https://host/?key=…`) is named after its host, so the name is never empty.
69
+ */
70
+ function queryFreeBaseName(url: URL): string {
71
+ const withoutQuery = new URL(url.pathname, url.origin).href;
72
+ return redoclyBaseName(withoutQuery) || url.hostname;
73
+ }
74
+
75
+ /** Each base name Redocly takes from a remote URL with a query, with the name to use instead. */
76
+ function queryBaseNames(documentRefs: Iterable<string>): Map<string, string> {
77
+ const names = new Map<string, string>();
78
+ for (const ref of documentRefs) {
79
+ const url = REMOTE_URL_RE.test(ref) && URL.canParse(ref) ? new URL(ref) : undefined;
80
+ if (url === undefined || url.search === "") {
81
+ continue;
82
+ }
83
+ const named = redoclyBaseName(ref);
84
+ const clean = queryFreeBaseName(url);
85
+ if (named !== clean) {
86
+ names.set(named, clean);
87
+ }
88
+ }
89
+ return names;
90
+ }
91
+
92
+ /** The name `key` gets, or undefined when no query base name starts it. */
93
+ function queryFreeName(key: string, baseNames: ReadonlyMap<string, string>): string | undefined {
94
+ // The longest base name first: `pet?a=1` must not match as `pet?a` when both were loaded.
95
+ const matches = [...baseNames.keys()].filter(
96
+ (named) =>
97
+ key === named || NAME_SUFFIX_STARTS.some((start) => key.startsWith(`${named}${start}`)),
98
+ );
99
+ const named = matches.sort((a, b) => b.length - a.length)[0];
100
+ return named === undefined ? undefined : `${baseNames.get(named)}${key.slice(named.length)}`;
101
+ }
102
+
103
+ function freeName(name: string, taken: ReadonlySet<string>): string {
104
+ let free = name;
105
+ for (let serial = FIRST_SERIAL; taken.has(free); serial += 1) {
106
+ free = `${name}-${serial}`;
107
+ }
108
+ return free;
109
+ }
110
+
111
+ /** Rename the bundled components of one group in place, keeping their order. */
112
+ function renameGroup(
113
+ entries: Record<string, unknown>,
114
+ keysBefore: ReadonlySet<string>,
115
+ baseNames: ReadonlyMap<string, string>,
116
+ ): Map<string, string> {
117
+ const renames = new Map<string, string>();
118
+ const taken = new Set(Object.keys(entries));
119
+ for (const key of Object.keys(entries)) {
120
+ const name = keysBefore.has(key) ? undefined : queryFreeName(key, baseNames);
121
+ if (name !== undefined) {
122
+ taken.delete(key);
123
+ const free = freeName(name, taken);
124
+ taken.add(free);
125
+ renames.set(key, free);
126
+ }
127
+ }
128
+ for (const [key, value] of Object.entries(entries)) {
129
+ delete entries[key];
130
+ entries[renames.get(key) ?? key] = value;
131
+ }
132
+ return renames;
133
+ }
134
+
135
+ /** Rewrites pointers and messages that name a renamed component. */
136
+ export interface ComponentRenaming {
137
+ /** The pointer with a renamed component's new name; any other text as it is. */
138
+ readonly pointer: (pointer: string) => string;
139
+ /** The text with each renamed component's old name replaced by its new name. */
140
+ readonly text: (text: string) => string;
141
+ }
142
+
143
+ function renamedPointer(pointer: string, renames: Renames): string {
144
+ if (!pointer.startsWith(`${ROOT_POINTER}/`)) {
145
+ return pointer;
146
+ }
147
+ const [components, group = "", name = "", ...rest] = parseRefPointer(canonicalRef(pointer));
148
+ const renamed = components === COMPONENTS_KEY ? renames.get(group)?.get(name) : undefined;
149
+ return renamed === undefined
150
+ ? pointer
151
+ : appendPointer(ROOT_POINTER, [COMPONENTS_KEY, group, renamed, ...rest]);
152
+ }
153
+
154
+ function renamedText(text: string, renames: Renames): string {
155
+ const pairs = [...renames.values()].flatMap((group) => [...group]);
156
+ // The longest old name first, so a name that starts another one does not split it.
157
+ pairs.sort(([a], [b]) => b.length - a.length);
158
+ return pairs.reduce((shown, [old, renamed]) => shown.replaceAll(old, renamed), text);
159
+ }
160
+
161
+ function rewriteRefs(document: unknown, renaming: ComponentRenaming): void {
162
+ for (const { node } of findRefHolders(document)) {
163
+ node.$ref = renaming.pointer(node.$ref);
164
+ }
165
+ for (const mapping of findMappings(document)) {
166
+ for (const [name, value] of Object.entries(mapping)) {
167
+ if (typeof value === "string") {
168
+ mapping[name] = renaming.pointer(value);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Rename, in place, the components the bundle named after a URL with a query, and every local
176
+ * `$ref` and `mapping` value that names them. `documentRefs` are the refs of the loaded documents.
177
+ */
178
+ export function renameQueryNamedComponents(
179
+ bundled: unknown,
180
+ keysBefore: ComponentKeys,
181
+ documentRefs: Iterable<string>,
182
+ ): ComponentRenaming {
183
+ const baseNames = queryBaseNames(documentRefs);
184
+ const renames: Renames = new Map();
185
+ if (baseNames.size > 0) {
186
+ for (const [group, entries] of Object.entries(componentsOf(bundled))) {
187
+ const groupRenames = renameGroup(entries, keysBefore.get(group) ?? new Set(), baseNames);
188
+ if (groupRenames.size > 0) {
189
+ renames.set(group, groupRenames);
190
+ }
191
+ }
192
+ }
193
+ const renaming: ComponentRenaming = {
194
+ pointer: (pointer) => renamedPointer(pointer, renames),
195
+ text: (text) => renamedText(text, renames),
196
+ };
197
+ if (renames.size > 0) {
198
+ rewriteRefs(bundled, renaming);
199
+ }
200
+ return renaming;
201
+ }
@@ -3,6 +3,7 @@ import {
3
3
  type BaseResolver,
4
4
  bundle,
5
5
  type Document,
6
+ type LocationObject,
6
7
  lintDocument,
7
8
  type NormalizedProblem,
8
9
  type Config as RedoclyConfig,
@@ -14,12 +15,19 @@ import type {
14
15
  SchemaLoadRequest,
15
16
  } from "../../application/ports/schema-loader.js";
16
17
  import { withStackOverflowReportedAsync } from "../../application/stack-overflow.js";
18
+ import { redactUrlSecrets } from "../../domain/url-secrets.js";
17
19
  import { WARNING_CODES, warningText } from "../../domain/warnings.js";
18
20
  import type { OpenAPI3 } from "../../types.js";
19
21
  import { removeBundledMappingCopies, schemaKeysOf } from "./bundled-mapping-copies.js";
20
22
  import { repairBundledRefs } from "./bundled-ref-repair.js";
21
23
  import { loadedDocuments, localFilesRead } from "./loaded-documents.js";
22
24
  import { findMalformedRefs, malformedRefsError } from "./malformed-refs.js";
25
+ import { pointMappingNamesAtSchemas } from "./mapping-schema-names.js";
26
+ import {
27
+ type ComponentRenaming,
28
+ componentKeysOf,
29
+ renameQueryNamedComponents,
30
+ } from "./query-named-components.js";
23
31
  import { RefAccessResolver } from "./ref-access.js";
24
32
  import { checkRefsResolve } from "./ref-resolution-check.js";
25
33
  import { REF_CYCLE_MESSAGE } from "./ref-resolution-steps.js";
@@ -160,6 +168,7 @@ class RedoclySchemaLoader implements SchemaLoader {
160
168
  const document = await parseSchema(request.source, { absoluteRef, resolver });
161
169
  logger.debug("Parsed schema", "redoc", now() - parseStartedAt);
162
170
  assertOpenAPI3(document.parsed);
171
+ pointMappingNamesAtSchemas(document.parsed);
163
172
  const bundled = await this.#lintAndBundle(document, resolver, request.shouldLint);
164
173
  const isRootAFile = request.source instanceof URL && request.source.protocol === "file:";
165
174
  const localFiles = await localFilesRead(document, resolver, isRootAFile);
@@ -171,6 +180,7 @@ class RedoclySchemaLoader implements SchemaLoader {
171
180
  const config = resolveConfigWithAccess(redoc.resolve, {
172
181
  timeoutMs: remoteRefTimeoutMs,
173
182
  access: request.refAccess,
183
+ rootUrl: request.source instanceof URL ? request.source.href : undefined,
174
184
  maxParallelFetches: remoteDocumentLimits.maxParallelFetches,
175
185
  bodyLimits: remoteDocumentLimits,
176
186
  });
@@ -202,13 +212,14 @@ class RedoclySchemaLoader implements SchemaLoader {
202
212
  config: this.#options.redoc.styleguide,
203
213
  externalRefResolver: resolver,
204
214
  });
205
- this.#report(problems);
215
+ this.#report(problems, document.source.absoluteRef);
206
216
  this.#options.logger.debug("Linted schema", "lint", this.#options.now() - startedAt);
207
217
  }
208
218
 
209
219
  async #bundle(document: Document, resolver: BaseResolver): Promise<OpenAPI3> {
210
220
  const startedAt = this.#options.now();
211
221
  const schemaKeys = schemaKeysOf(document.parsed);
222
+ const componentKeys = componentKeysOf(document.parsed);
212
223
  // The lint resolver: documents it already loaded are not fetched again.
213
224
  const bundled = await bundle({
214
225
  config: this.#options.redoc,
@@ -216,9 +227,12 @@ class RedoclySchemaLoader implements SchemaLoader {
216
227
  doc: document,
217
228
  externalRefResolver: resolver,
218
229
  });
219
- this.#report(bundled.problems);
220
230
  removeBundledMappingCopies(bundled.bundle.parsed, schemaKeys);
221
231
  repairBundledRefs(bundled.bundle.parsed);
232
+ const documentRefs = resolver.getFiles();
233
+ const renaming = renameQueryNamedComponents(bundled.bundle.parsed, componentKeys, documentRefs);
234
+ // The problems name the components the bundle made, so they are shown by their new names.
235
+ this.#report(bundled.problems, document.source.absoluteRef, renaming);
222
236
  this.#options.logger.debug("Bundled schema", "bundle", this.#options.now() - startedAt);
223
237
  return bundled.bundle.parsed;
224
238
  }
@@ -228,22 +242,22 @@ class RedoclySchemaLoader implements SchemaLoader {
228
242
  * config's `.redocly.lint-ignore.yaml` lists is left out, as `redocly lint` leaves it out
229
243
  * (upstream #2189).
230
244
  */
231
- #report(problems: readonly NormalizedProblem[]): void {
245
+ #report(problems: readonly NormalizedProblem[], rootRef: string, renaming = UNRENAMED): void {
232
246
  const errorMessages: string[] = [];
233
247
  for (const problem of problems) {
234
248
  if (problem.ignored === true) {
235
249
  this.#options.logger.debug(
236
- `Ignored by the lint ignore file: ${problemMessage(problem)}`,
250
+ `Ignored by the lint ignore file: ${problemMessage(problem, renaming, rootRef)}`,
237
251
  "lint",
238
252
  );
239
253
  continue;
240
254
  }
241
- const message = problemMessage(problem);
255
+ const message = problemMessage(problem, renaming, rootRef);
242
256
  if (problem.severity === "error") {
243
257
  errorMessages.push(message);
244
258
  this.#options.logger.error(message);
245
259
  } else {
246
- this.#options.logger.warn(lintWarningText(problem));
260
+ this.#options.logger.warn(lintWarningText(message, problem.ruleId));
247
261
  }
248
262
  }
249
263
  if (errorMessages.length > 0) {
@@ -253,16 +267,34 @@ class RedoclySchemaLoader implements SchemaLoader {
253
267
  }
254
268
 
255
269
  /** A lint warning names its rule, which `redocly.yaml` can turn off, and ends with its code. */
256
- function lintWarningText(problem: NormalizedProblem): string {
257
- return warningText(
258
- `${problemMessage(problem)}, rule ${problem.ruleId}`,
259
- WARNING_CODES.LINT_PROBLEM,
260
- );
270
+ function lintWarningText(message: string, ruleId: string): string {
271
+ return warningText(`${message}, rule ${ruleId}`, WARNING_CODES.LINT_PROBLEM);
272
+ }
273
+
274
+ /** Problems found before the bundle named any component. */
275
+ const UNRENAMED: ComponentRenaming = { pointer: (pointer) => pointer, text: (text) => text };
276
+
277
+ /** A problem location that names a JSON Pointer, not a line and column. */
278
+ type PointerLocation = Extract<LocationObject, { pointer: string }>;
279
+
280
+ /**
281
+ * Where a problem is: its pointer, and the document when it is not the root (URL secrets left
282
+ * out).
283
+ */
284
+ function locationText(location: PointerLocation, renaming: ComponentRenaming, rootRef: string) {
285
+ const pointer = renaming.pointer(location.pointer);
286
+ const documentRef = location.source.absoluteRef;
287
+ return documentRef === rootRef ? pointer : `${pointer} in ${redactUrlSecrets(documentRef)}`;
261
288
  }
262
289
 
263
- function problemMessage(problem: NormalizedProblem): string {
264
- const location = problem.location?.[0]?.pointer;
265
- return location ? `${problem.message} at ${location}` : problem.message;
290
+ function problemMessage(
291
+ problem: NormalizedProblem,
292
+ renaming: ComponentRenaming,
293
+ rootRef: string,
294
+ ): string {
295
+ const location = problem.location?.[0];
296
+ const message = renaming.text(problem.message);
297
+ return location?.pointer ? `${message} at ${locationText(location, renaming, rootRef)}` : message;
266
298
  }
267
299
 
268
300
  /** A schema loader with one Redocly config and logger, for one or more loads. */
@@ -13,6 +13,7 @@ import { ERROR_CODES, type OpenAPITypesafeError } from "../../domain/errors.js";
13
13
  import { redactUrlSecrets } from "../../domain/url-secrets.js";
14
14
  import { aliasExpansionFailure } from "./alias-limit.js";
15
15
  import { isHostAllowed } from "./allowed-hosts.js";
16
+ import { pointMappingNamesAtSchemas } from "./mapping-schema-names.js";
16
17
  import { isParsedSource, RemoteNodeLimit } from "./remote-node-limit.js";
17
18
  import { refForbiddenError } from "./schema-load-errors.js";
18
19
 
@@ -162,21 +163,33 @@ export class RefAccessResolver extends BaseResolver {
162
163
  return super.resolveDocument(base, ref, isRoot);
163
164
  }
164
165
 
165
- /** Refuse a YAML alias bomb before Redocly walks it (pre-beta test plan 10.2). */
166
+ /**
167
+ * Refuse a YAML alias bomb before Redocly walks it (pre-beta test plan 10.2). A document a
168
+ * `$ref` loads has its own `components.schemas`, so its mapping names point at those schemas
169
+ * before Redocly resolves them, as the root's do (`pointMappingNamesAtSchemas`).
170
+ */
166
171
  override parseDocument(source: Source, isRoot = false): Document {
167
172
  const document = this.#parsed(source, isRoot);
168
173
  const failure = aliasExpansionFailure(document.parsed);
169
174
  if (failure !== undefined) {
170
175
  throw new YamlParseError(failure, source);
171
176
  }
177
+ pointMappingNamesAtSchemas(document.parsed);
172
178
  return document;
173
179
  }
174
180
 
175
- /** A remote document is parsed within the node limit; any other one as Redocly parses it. */
181
+ /**
182
+ * A remote document is parsed within the node limit; a local one as Redocly parses it. Whether
183
+ * a remote one is parsed at all is decided here (`isParsedSource`), since Redocly read the
184
+ * extension of a URL with its query.
185
+ */
176
186
  #parsed(source: Source, isRoot: boolean): Document {
177
- if (!isRemoteRef(source.absoluteRef) || !isParsedSource(source, isRoot)) {
187
+ if (!isRemoteRef(source.absoluteRef)) {
178
188
  return super.parseDocument(source, isRoot);
179
189
  }
190
+ if (!isParsedSource(source, isRoot)) {
191
+ return { source, parsed: source.body };
192
+ }
180
193
  try {
181
194
  return this.#nodeLimit.parse(source);
182
195
  } catch (error) {
@@ -6,38 +6,99 @@ export interface RefHolder {
6
6
  readonly pointer: string;
7
7
  }
8
8
 
9
+ /**
10
+ * Keywords whose value is data, not OpenAPI: a `$ref` or a `discriminator` in an example, a
11
+ * default, a `const` or an `enum` value, or an extension is part of the data and stays as written.
12
+ */
13
+ const DATA_KEYWORDS = new Set(["example", "examples", "default", "const", "enum"]);
14
+ const EXTENSION_PREFIX = "x-";
15
+
16
+ /**
17
+ * Keywords whose value maps names to objects: a property, a schema or a response may be named
18
+ * `default` or `example`, and is OpenAPI all the same.
19
+ */
20
+ const NAME_MAPS = new Set([
21
+ "properties",
22
+ "patternProperties",
23
+ "dependentSchemas",
24
+ "$defs",
25
+ "definitions",
26
+ "schemas",
27
+ "responses",
28
+ "parameters",
29
+ "requestBodies",
30
+ "headers",
31
+ "securitySchemes",
32
+ "links",
33
+ "callbacks",
34
+ "pathItems",
35
+ "mediaTypes",
36
+ "content",
37
+ "encoding",
38
+ "paths",
39
+ "webhooks",
40
+ ]);
41
+
9
42
  interface PendingNode {
10
43
  readonly value: object;
11
44
  readonly pointer: string;
45
+ /** Whether the keys of `value` are names (of properties, schemas, responses), not keywords. */
46
+ readonly hasNameKeys: boolean;
12
47
  }
13
48
 
14
- function hasStringRef(value: object): value is { $ref: string } {
15
- return !Array.isArray(value) && typeof (value as { $ref?: unknown }).$ref === "string";
49
+ function isDataKey(key: string): boolean {
50
+ return DATA_KEYWORDS.has(key) || key.startsWith(EXTENSION_PREFIX);
16
51
  }
17
52
 
18
53
  /**
19
- * Every object with a string `$ref` in a parsed document, in no particular order. The walk uses
20
- * its own stack, so a deeply nested document does not run out of call stack, and a node shared by
21
- * YAML aliases (or a cycle of them) is visited once.
54
+ * Every object of a parsed document that is OpenAPI, not data (`DATA_KEYWORDS`), with its JSON
55
+ * Pointer, in no particular order. The walk uses its own stack, so a deeply nested document does
56
+ * not run out of call stack, and a node shared by YAML aliases (or a cycle of them) is visited
57
+ * once.
22
58
  */
23
- export function findRefHolders(document: unknown): RefHolder[] {
24
- const found: RefHolder[] = [];
59
+ function openApiNodes(document: unknown): PendingNode[] {
60
+ const found: PendingNode[] = [];
25
61
  const visited = new WeakSet<object>();
26
62
  const pending: PendingNode[] = [];
27
- const enqueue = (value: unknown, pointer: string): void => {
63
+ const enqueue = (value: unknown, pointer: string, hasNameKeys: boolean): void => {
28
64
  if (typeof value === "object" && value !== null && !visited.has(value)) {
29
65
  visited.add(value);
30
- pending.push({ value, pointer });
66
+ pending.push({ value, pointer, hasNameKeys });
31
67
  }
32
68
  };
33
- enqueue(document, ROOT_POINTER);
69
+ enqueue(document, ROOT_POINTER, false);
34
70
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
35
- if (hasStringRef(next.value)) {
36
- found.push({ node: next.value, pointer: next.pointer });
37
- }
71
+ found.push(next);
72
+ // One push per child: spreading an array of millions of children overflows the stack.
38
73
  for (const [key, child] of Object.entries(next.value)) {
39
- enqueue(child, childPointer(next.pointer, key));
74
+ if (next.hasNameKeys || !isDataKey(key)) {
75
+ enqueue(child, childPointer(next.pointer, key), !next.hasNameKeys && NAME_MAPS.has(key));
76
+ }
40
77
  }
41
78
  }
42
79
  return found;
43
80
  }
81
+
82
+ function hasStringRef(value: object): value is { $ref: string } {
83
+ return !Array.isArray(value) && typeof (value as { $ref?: unknown }).$ref === "string";
84
+ }
85
+
86
+ /** Every object with a string `$ref` in a parsed document, outside data, in no particular order. */
87
+ export function findRefHolders(document: unknown): RefHolder[] {
88
+ return openApiNodes(document).flatMap(({ value, pointer, hasNameKeys }) =>
89
+ !hasNameKeys && hasStringRef(value) ? [{ node: value, pointer }] : [],
90
+ );
91
+ }
92
+
93
+ function isMapping(value: unknown): value is Record<string, unknown> {
94
+ return typeof value === "object" && value !== null && !Array.isArray(value);
95
+ }
96
+
97
+ /** Every `discriminator.mapping` object of a parsed document, outside data. */
98
+ export function findMappings(document: unknown): Record<string, unknown>[] {
99
+ return openApiNodes(document).flatMap(({ value, hasNameKeys }) => {
100
+ const { discriminator } = value as { discriminator?: unknown };
101
+ const mapping = !hasNameKeys && isMapping(discriminator) ? discriminator.mapping : undefined;
102
+ return isMapping(mapping) ? [mapping] : [];
103
+ });
104
+ }
@@ -115,6 +115,11 @@ export interface RedirectPolicy {
115
115
  readonly headers: readonly ConfiguredHeader[];
116
116
  /** `--remote-ref-hosts`; undefined allows every host. */
117
117
  readonly hosts: readonly string[] | undefined;
118
+ /**
119
+ * The URL of the root document, if it is remote. The developer chose it, so neither it nor its
120
+ * redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.
121
+ */
122
+ readonly rootUrl: string | undefined;
118
123
  }
119
124
 
120
125
  /**
@@ -130,12 +135,13 @@ export function fetchWithCheckedRedirects(
130
135
  policy: RedirectPolicy,
131
136
  ): RemoteFetch {
132
137
  return async (input, init = {}) => {
138
+ const hosts = input === policy.rootUrl ? undefined : policy.hosts;
133
139
  let url = new URL(input);
134
140
  for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
135
- if (hop > 0 && policy.hosts !== undefined && !isHostAllowed(url, policy.hosts)) {
141
+ if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {
136
142
  throw refForbiddenError(url.href, {
137
143
  reason: "host-not-allowed",
138
- allowedHosts: [...policy.hosts],
144
+ allowedHosts: [...hosts],
139
145
  });
140
146
  }
141
147
  const headers = await headersFor(url, policy.headers);
@@ -190,6 +196,8 @@ export interface RemoteFetchOptions {
190
196
  /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */
191
197
  readonly timeoutMs: number;
192
198
  readonly access: RefAccess;
199
+ /** The root document's URL, whose redirects are not limited to the hosts. */
200
+ readonly rootUrl: string | undefined;
193
201
  readonly maxParallelFetches: number;
194
202
  readonly bodyLimits: RemoteBodyLimits;
195
203
  }
@@ -216,6 +224,7 @@ export function resolveConfigWithAccess(
216
224
  const checked = fetchWithCheckedRedirects(base, {
217
225
  headers: resolve.http.headers,
218
226
  hosts: options.access.remoteHosts,
227
+ rootUrl: options.rootUrl,
219
228
  });
220
229
  // The limit is outside the redirects, so all hops of one document share one slot.
221
230
  const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);
@@ -13,12 +13,52 @@ import { refForbiddenError } from "./schema-load-errors.js";
13
13
  /** The extensions that Redocly 1.34's `BaseResolver.parseDocument` parses as YAML or JSON. */
14
14
  const PARSED_EXTENSIONS: ReadonlySet<string> = new Set([".json", ".yml", ".yaml"]);
15
15
  const PARSED_MIME_TYPE = /(json|yaml|openapi)/;
16
+ /**
17
+ * The text formats that a `$ref` loads as text, as a `description` from a Markdown file. A URL
18
+ * keeps its document as text only when its extension or type names one of them: the last segment
19
+ * of an API's path often has a dot that is no file type (`/schemas/v1.0`, `/schemas/pet.v2`).
20
+ */
21
+ const TEXT_EXTENSIONS: ReadonlySet<string> = new Set([".md", ".markdown", ".txt", ".html", ".htm"]);
22
+ const TEXT_MIME_TYPE = /^text\/(markdown|x-markdown|html)\b/;
23
+ const REMOTE_URL_RE = /^https?:\/\//;
24
+ const PATH_SEPARATOR_RE = /[/\\]/;
25
+ const EXTENSION_START = ".";
26
+
27
+ /** The extension of the last segment of a path, with its dot; empty when it has none. */
28
+ function extensionOf(path: string): string {
29
+ const name = path.split(PATH_SEPARATOR_RE).at(-1) ?? "";
30
+ const start = name.lastIndexOf(EXTENSION_START);
31
+ return start === -1 ? "" : name.slice(start);
32
+ }
33
+
34
+ /**
35
+ * The path of a document: the path of a URL, without its query and fragment, or the file path.
36
+ * Redocly took the extension of the whole URL, so `pet.yaml?token=…` had none it knew.
37
+ */
38
+ function documentPath(absoluteRef: string): string {
39
+ return REMOTE_URL_RE.test(absoluteRef) && URL.canParse(absoluteRef)
40
+ ? new URL(absoluteRef).pathname
41
+ : absoluteRef;
42
+ }
16
43
 
17
- /** Whether Redocly parses this source, or keeps its text: the same rule as its own resolver. */
44
+ /**
45
+ * Whether a source is parsed as YAML or JSON, or kept as text (a Markdown `description`, say).
46
+ * It is parsed when it is the root, when its type or the extension of its path says JSON or YAML,
47
+ * and when it is a URL whose path and type name no text format: an API serves its documents at
48
+ * paths like `/schemas/pet` or `/schemas/v1.0`, often as `text/plain`, and Redocly kept those as
49
+ * text, which failed as a schema. A local file is kept as text unless its name says JSON or YAML,
50
+ * as Redocly keeps it.
51
+ */
18
52
  export function isParsedSource(source: Source, isRoot: boolean): boolean {
19
- const { absoluteRef, mimeType } = source;
20
- const extension = absoluteRef.slice(absoluteRef.lastIndexOf("."));
21
- return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? "");
53
+ const { absoluteRef, mimeType = "" } = source;
54
+ const extension = extensionOf(documentPath(absoluteRef));
55
+ const isTextUrl = TEXT_EXTENSIONS.has(extension) || TEXT_MIME_TYPE.test(mimeType);
56
+ return (
57
+ isRoot ||
58
+ PARSED_EXTENSIONS.has(extension) ||
59
+ PARSED_MIME_TYPE.test(mimeType) ||
60
+ (REMOTE_URL_RE.test(absoluteRef) && !isTextUrl)
61
+ );
22
62
  }
23
63
 
24
64
  /** The parser's event for a node it starts to build. */
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import {
5
5
  type BaseResolver,
6
6
  type Document,
7
- makeDocumentFromString,
7
+ parseYaml,
8
8
  ResolveError,
9
9
  Source,
10
10
  YamlParseError,
@@ -74,12 +74,24 @@ function parseJsonText(text: string, absoluteRef: string): Document {
74
74
  }
75
75
  }
76
76
 
77
+ /**
78
+ * How a parser message names document text that is no file. The document itself is named as a
79
+ * file in the working directory, so that its relative `$ref`s resolve there, but no such file
80
+ * exists, and a message that named it sent the user looking for it.
81
+ */
82
+ const TEXT_ORIGIN_NAMES: Readonly<Record<TextOrigin, string>> = {
83
+ string: "the schema string",
84
+ stream: "the schema stream",
85
+ };
86
+
77
87
  function parseYamlText(text: string, absoluteRef: string, origin: TextOrigin): Document {
88
+ const source = new Source(absoluteRef, text);
78
89
  let document: Document;
79
90
  try {
80
- document = makeDocumentFromString(text, absoluteRef);
91
+ document = { source, parsed: parseYaml(text, { filename: TEXT_ORIGIN_NAMES[origin] }) };
81
92
  } catch (cause) {
82
- throw parseFailedError("yaml", absoluteRef, cause);
93
+ // As Redocly's own parse: the error keeps the first line, not the parser's code excerpt.
94
+ throw parseFailedError("yaml", absoluteRef, new YamlParseError(cause as Error, source));
83
95
  }
84
96
  // Text that is only a YAML string scalar, such as "./openapi.yaml", was meant as a file path.
85
97
  if (typeof document.parsed === "string") {