@sous-io/sous 0.2.36 → 1.0.1

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 (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -237
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -391
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -412
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1105
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,1028 +0,0 @@
1
- /**
2
- * The resolver.
3
- *
4
- * Given what a project asked for (its subscriptions, or the refs named on a
5
- * command line), the resolver decides exactly which recipe versions that means.
6
- * It works the way apt does: a ref is looked up across the cached indexes of
7
- * EVERY added repository at once, and a ref that genuinely exists in more than
8
- * one of them is an error demanding the qualified form, never a silent
9
- * first-match-wins.
10
- *
11
- * It then walks the dependency closure. Each resolved recipe's manifest names
12
- * more refs, through `depends` (fetched and addressable, but not added to the
13
- * project) and `subscribes` (co-subscribed with full semantics), and those refs
14
- * resolve the same way. The manifest lives inside the recipe's own files, so
15
- * the caller supplies a loader; that is also the seam where a caller decides
16
- * whether it is willing to fetch anything at this point. When the loader has no
17
- * manifest to give, the lists are read from the version's index entry instead,
18
- * which records the entry that declared each dependency; only a version whose
19
- * entry predates that record goes unwalked.
20
- *
21
- * Two things the resolver deliberately does NOT do: it never downloads
22
- * anything, and it never resolves against a repository the project has not
23
- * added. A dependency on an unknown repository comes back as a `MissingRepo`
24
- * carrying its provenance, so the trust layer can ask about it by name.
25
- */
26
-
27
- import semver from "semver";
28
- import { ConfigError } from "../errors.js";
29
- import type { IndexFile } from "./formats/index-file.js";
30
- import type { LockKind } from "./formats/lockfile.js";
31
- import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
32
- import { indexDependencyLists, type DependencyLists } from "./declarations.js";
33
- import {
34
- CatalogMatcher,
35
- RefSource,
36
- catalogRepoOfIndex,
37
- formatRef,
38
- isNamedRef,
39
- locationOf,
40
- namespaceOf,
41
- namespaceRef,
42
- rangeOf,
43
- recipeRef,
44
- repoOf,
45
- settleDependency,
46
- sharedRefResolver,
47
- shortKey,
48
- splitRecipeKey,
49
- withRepoName,
50
- type NamedRef,
51
- type RepoRef,
52
- type SousRef,
53
- } from "../../services/ref-resolver/index.js";
54
- import { shortNameFromIdentity } from "./identity.js";
55
- import type { IndexDependency } from "./formats/index-file.js";
56
-
57
- /** The `requestedBy` holder meaning "the project asked for this directly". */
58
- export const PROJECT_REQUESTER = "project";
59
-
60
- /** One thing to resolve. */
61
- export type RefRequest = {
62
- /** The ref, already parsed. */
63
- ref: NamedRef;
64
- /**
65
- * The version range every recipe of a namespace ref must satisfy, and the
66
- * range of a recipe ref that carries none of its own.
67
- */
68
- range?: string;
69
- /** Whether prerelease versions may take part in range matching. */
70
- prerelease?: boolean;
71
- /**
72
- * Who asked: the literal "project" for a subscription the project holds
73
- * directly, or the recipe key of the recipe whose manifest asked.
74
- */
75
- requestedBy: string;
76
- /**
77
- * Whether this is a co-subscription or a build dependency. Defaults to
78
- * "subscribes", which is what a project's own subscriptions are.
79
- */
80
- kind?: LockKind;
81
- };
82
-
83
- /** What the resolver needs to know about a repository the project has added. */
84
- export type ResolverRepo = {
85
- /** Where the repository lives. */
86
- url: string;
87
- /**
88
- * The repository's canonical identity, `<host>/<owner path>/<name>`. A
89
- * dependency that names another repository names it by location, so this is
90
- * what decides whether the project has already added it, whatever short name
91
- * the project gave it.
92
- */
93
- identity: string;
94
- /** The provider it names, when it names one. */
95
- provider?: string;
96
- /** Whether it prefers a newer in-range version over the locked one. */
97
- alwaysPull?: boolean;
98
- };
99
-
100
- /**
101
- * Loads a resolved recipe's manifest, which is what names its dependencies.
102
- * Returning undefined means "not available": the recipe's dependencies are then
103
- * read from its index entry, and when that does not record them either, it is
104
- * reported under `missingManifests` rather than having its dependencies walked.
105
- */
106
- export type RecipeManifestLoader = (
107
- recipe: ResolvedRecipe
108
- ) => Promise<RecipeManifest | undefined> | RecipeManifest | undefined;
109
-
110
- /** Everything the resolver reads. */
111
- export type ResolveContext = {
112
- /** The cached index of every added repository, keyed by its short name. */
113
- indexes: Map<string, IndexFile>;
114
- /** The added repositories, keyed by short name; the trust list, in other words. */
115
- repos: Record<string, ResolverRepo>;
116
- /** How a resolved recipe's manifest is loaded. */
117
- loadManifest: RecipeManifestLoader;
118
- /** Whether prereleases are allowed when a request does not say. Defaults to false. */
119
- prerelease?: boolean;
120
- /**
121
- * Versions to hold where they are, keyed by recipe key. A recipe named here
122
- * resolves to that version whenever it still satisfies every range asked of
123
- * it, rather than to the newest one that does; a range that no longer allows
124
- * it wins, and the newest satisfying version is chosen as usual. This is how
125
- * an update moves only the pins it was asked to move.
126
- */
127
- keep?: Record<string, string>;
128
- };
129
-
130
- /** One recipe version the resolver settled on. */
131
- export type ResolvedRecipe = {
132
- /** The recipe key, `namespace/recipe`. */
133
- key: string;
134
- namespace: string;
135
- name: string;
136
- /** The short name of the repository it resolves in, as this project calls it. */
137
- repo: string;
138
- /** That repository's canonical identity, which the machine-wide store is keyed by. */
139
- identity: string;
140
- /** The exact version chosen. */
141
- version: string;
142
- /** The content hash the index publishes for that version. */
143
- hash: string;
144
- /** The git tag carrying that version. */
145
- tag: string;
146
- /** The recipe folder, relative to the repository root. */
147
- path: string;
148
- /** Whether anything holds it as a co-subscription; otherwise a build dependency. */
149
- kind: LockKind;
150
- /** Everyone holding it: "project", and the key of every recipe that asked. */
151
- requestedBy: string[];
152
- /** Every range that had to be satisfied at once, with who asked for it. */
153
- ranges: Array<{ range: string; requestedBy: string }>;
154
- /** Whether prereleases took part in the match. */
155
- prerelease: boolean;
156
- /**
157
- * What the repository's index says this exact version depends on, when the
158
- * index records it. These are the versions the release resolved, so a
159
- * dependency of an indexed recipe is installed at the version it was
160
- * published against rather than at whatever its range would reach today.
161
- */
162
- dependencies?: Record<string, IndexDependency>;
163
- /**
164
- * The variable definitions the index records for this exact version, when it
165
- * records them: the questions it asks, known before its files are fetched.
166
- */
167
- variables?: VariableDefinition[];
168
- /**
169
- * The manifest `depends` and `subscribes` lists the index records for this
170
- * exact version, when it records them, so the recipe can be walked before its
171
- * files are fetched.
172
- */
173
- declares?: DependencyLists;
174
- };
175
-
176
- /** A repository something needs that the project has not added. */
177
- export type MissingRepo = {
178
- /**
179
- * The short name to record it under: the one a ref qualified it with, or one
180
- * derived from its location when a dependency named it by URL.
181
- */
182
- name: string;
183
- /** Its URL, which a dependency's locator carries. */
184
- url?: string;
185
- /** Its canonical identity, when the dependency named a location. */
186
- identity?: string;
187
- /** The provider its locator named, when it named one. */
188
- provider?: string;
189
- /** Every ref that needs it, and who asked for that ref. */
190
- requiredBy: Array<{ ref: string; requestedBy: string }>;
191
- };
192
-
193
- /** What a resolution produced. */
194
- export type ResolveResult = {
195
- /** Every recipe version to install, in a stable order (by key). */
196
- resolved: ResolvedRecipe[];
197
- /** Repositories a dependency needs that the project has not added. */
198
- missingRepos: MissingRepo[];
199
- /**
200
- * Resolved recipes whose dependencies could not be read: the loader produced
201
- * no manifest, and the index entry does not record them.
202
- */
203
- missingManifests: string[];
204
- /** Any dependency cycle found, as the chain of recipe keys forming it. */
205
- cycles: string[][];
206
- };
207
-
208
- /** A pending piece of work: one ref to resolve on behalf of one holder. */
209
- type WorkItem = RefRequest & {
210
- kind: LockKind;
211
- /**
212
- * Where the ref said the recipe lives, when a manifest named another
213
- * repository by location. The project may already have added that repository
214
- * under any short name at all, so it is matched by identity.
215
- */
216
- remote?: {
217
- /** The repository's canonical identity. */
218
- identity: string;
219
- /** Its HTTPS location, which is what adding it would be handed. */
220
- url: string;
221
- /** The provider the locator named. */
222
- provider?: string;
223
- /** The dependency exactly as the manifest wrote it, for messages. */
224
- written: string;
225
- /**
226
- * The repository ref a browser path dependency was read as (its location,
227
- * its path and the range the manifest wrote), settled through the
228
- * repository's index once the project has it. Until then `ref` holds no
229
- * namespace.
230
- */
231
- browsed?: RepoRef;
232
- /** What the declaring version's index records, for pins once a browsed path is settled. */
233
- pins?: Record<string, IndexDependency>;
234
- };
235
- };
236
-
237
- /**
238
- * Turns one entry of a manifest's `depends` or `subscribes` into a piece of
239
- * work.
240
- *
241
- * Two things are decided here. A SIBLING ref resolves inside the declaring
242
- * recipe's own repository, never across the others, because that is what
243
- * writing it without a location means. And when the repository's index records
244
- * what this exact version of the parent was released against, that exact
245
- * version is what gets asked for, rather than whatever the declared range would
246
- * reach today.
247
- *
248
- * A dependency that reads more than one way (a GitLab URL with nested groups)
249
- * is settled by what the release recorded in the index, and failing that by
250
- * what the project's own cached indexes already publish; nothing is fetched to
251
- * find out. One that still reads several ways is an error naming them.
252
- *
253
- * @param written - The dependency as the manifest wrote it.
254
- * @param parent - The recipe whose manifest declared it.
255
- * @param kind - Whether it was declared as a dependency or a co-subscription.
256
- * @param context - The added repositories and their cached indexes.
257
- */
258
- function dependencyRequest(
259
- written: string,
260
- parent: ResolvedRecipe,
261
- kind: LockKind,
262
- context: ResolveContext
263
- ): WorkItem {
264
- const reading = settleDependency(written, {
265
- ...(parent.dependencies === undefined ? {} : { recorded: parent.dependencies }),
266
- known: (candidate) => knownLocally(candidate, context),
267
- });
268
- if (reading === undefined) throw unsettledDependencyError(written, parent);
269
-
270
- const base = {
271
- requestedBy: parent.key,
272
- kind,
273
- ...(parent.prerelease ? { prerelease: true } : {}),
274
- };
275
-
276
- if (reading.kind === "repo" && reading.browsed !== undefined) {
277
- return {
278
- ref: namespaceRef(""),
279
- ...base,
280
- remote: {
281
- ...remoteOf(reading, written),
282
- browsed: reading,
283
- ...(parent.dependencies === undefined ? {} : { pins: parent.dependencies }),
284
- },
285
- };
286
- }
287
- /* c8 ignore next */
288
- if (!isNamedRef(reading)) throw unsettledDependencyError(written, parent);
289
-
290
- const pinned = parent.dependencies?.[shortKey(reading)];
291
- const ref = pinnedRef(reading, pinned);
292
-
293
- if (locationOf(reading) === undefined) {
294
- return { ref: withRepoName(ref, parent.repo), ...base };
295
- }
296
-
297
- const remote = remoteOf(reading, written);
298
- return {
299
- ref,
300
- ...base,
301
- remote: { ...remote, identity: pinned?.repo ?? remote.identity },
302
- };
303
- }
304
-
305
- /**
306
- * A named reading as a request asks for it: at the exact version the index
307
- * recorded when it recorded one, otherwise at the range the manifest wrote.
308
- *
309
- * @param reading - The reading.
310
- * @param pinned - What the declaring version's index recorded for it.
311
- */
312
- function pinnedRef(reading: NamedRef, pinned: IndexDependency | undefined): NamedRef {
313
- const namespace = namespaceOf(reading)!.name;
314
- if (reading.kind === "namespace") return namespaceRef(namespace);
315
- const range = pinned?.version ?? reading.range ?? pinned?.range;
316
- return recipeRef(namespace, reading.name, range === undefined ? {} : { range });
317
- }
318
-
319
- /** Where a located reading's repository lives, as a request carries it. */
320
- function remoteOf(
321
- reading: SousRef,
322
- written: string
323
- ): NonNullable<WorkItem["remote"]> {
324
- const location = locationOf(reading)!;
325
- return {
326
- identity: location.identity,
327
- url: location.url,
328
- provider: location.provider,
329
- written: written.trim(),
330
- };
331
- }
332
-
333
- /**
334
- * Whether a reading's repository is one the project added and its cached index
335
- * publishes what the reading names. It reads only what is already here.
336
- *
337
- * @param reading - One reading of a dependency.
338
- * @param context - The added repositories and their cached indexes.
339
- */
340
- function knownLocally(reading: SousRef, context: ResolveContext): boolean {
341
- const location = locationOf(reading);
342
- if (location === undefined) return false;
343
- const added = repoNamedByIdentity(context.repos, location.identity);
344
- const index = added === undefined ? undefined : context.indexes.get(added);
345
- if (index === undefined) return false;
346
- return new CatalogMatcher([catalogRepoOfIndex(index, { location })]).match(reading).length > 0;
347
- }
348
-
349
- /**
350
- * The error for a dependency that reads more than one way with nothing to
351
- * settle it.
352
- *
353
- * @param written - The dependency as written.
354
- * @param parent - The recipe that declared it.
355
- */
356
- function unsettledDependencyError(written: string, parent: ResolvedRecipe): ConfigError {
357
- const { refs } = sharedRefResolver().parse(written, RefSource.Manifest);
358
- return new ConfigError(
359
- `The dependency '${written.trim()}' of the recipe '${parent.key}' can be read ` +
360
- `${refs.length} ways, and nothing says which one it means:\n` +
361
- refs.map((reading) => ` ${formatRef(reading)}`).join("\n") +
362
- `\n A release records which one it settled on in the index; the index of ` +
363
- `'${parent.repo}' records none for this version. Release the recipe again with this ` +
364
- `version of sous, or write the dependency so it reads one way, as one of the forms above.`
365
- );
366
- }
367
-
368
- /**
369
- * Settles a browsed path, once the project has the repository it names.
370
- *
371
- * @param item - The request, whose `ref` is filled in.
372
- * @param index - The repository's cached index.
373
- */
374
- function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
375
- const remote = item.remote!;
376
- const browsed = remote.browsed!;
377
- const settled =
378
- index === undefined
379
- ? []
380
- : new CatalogMatcher([catalogRepoOfIndex(index, { location: browsed.location })])
381
- .match(browsed)
382
- .map((match) => match.ref)
383
- .filter(isNamedRef);
384
-
385
- let found: NamedRef | undefined = settled.length === 1 ? settled[0] : undefined;
386
- if (found === undefined) {
387
- // The folder may have moved since the release; the key it settled on then
388
- // is recorded beside the repository it lives in.
389
- const recorded = Object.keys(remote.pins ?? {}).filter(
390
- (key) => remote.pins![key]!.repo === remote.identity
391
- );
392
- if (recorded.length === 1) {
393
- const { namespace, name } = splitRecipeKey(recorded[0]!);
394
- found = recipeRef(namespace, name);
395
- }
396
- }
397
-
398
- if (found === undefined) {
399
- throw new ConfigError(
400
- `The dependency '${remote.written}' names a folder, and the index of the repository at ` +
401
- `${remote.url} ${settled.length === 0 ? "publishes no recipe there" : "publishes several recipes there"}.` +
402
- `\n It was required by the recipe '${item.requestedBy}'. Write the dependency as the ` +
403
- `recipe it means, such as '${remote.url}/<namespace>/<recipe>'.`
404
- );
405
- }
406
-
407
- const withRange =
408
- found.kind === "recipe" && browsed.range !== undefined ? { ...found, range: browsed.range } : found;
409
- item.ref = withRepoName(pinnedRef(withRange, remote.pins?.[shortKey(found)]), repoOf(item.ref)?.name);
410
- }
411
-
412
- /**
413
- * The short name of the added repository with this identity, or undefined when
414
- * the project has added none. Short names are a project's own labels, so the
415
- * identity is what a dependency is matched on.
416
- *
417
- * @param repos - The repositories the project has added.
418
- * @param identity - The canonical identity to look for.
419
- */
420
- export function repoNamedByIdentity(
421
- repos: Record<string, ResolverRepo>,
422
- identity: string
423
- ): string | undefined {
424
- for (const [name, entry] of Object.entries(repos)) {
425
- if (entry.identity === identity) return name;
426
- }
427
- return undefined;
428
- }
429
-
430
- /**
431
- * A short name for a repository the project has not added yet, derived from its
432
- * location and made unique against the names already in use.
433
- *
434
- * @param repos - The repositories the project has added.
435
- * @param identity - The canonical identity of the repository being named.
436
- */
437
- export function proposeRepoName(
438
- repos: Record<string, ResolverRepo>,
439
- identity: string
440
- ): string {
441
- const base = shortNameFromIdentity(identity);
442
- if (!Object.hasOwn(repos, base)) return base;
443
- for (let suffix = 2; ; suffix++) {
444
- const candidate = `${base}-${suffix}`;
445
- if (!Object.hasOwn(repos, candidate)) return candidate;
446
- }
447
- }
448
-
449
- /**
450
- * Resolves a set of refs and the whole dependency closure beneath them.
451
- *
452
- * @param requests - What the project (or the command line) asked for.
453
- * @param context - The cached indexes, the added repositories and a manifest loader.
454
- */
455
- export async function resolveRefs(
456
- requests: RefRequest[],
457
- context: ResolveContext
458
- ): Promise<ResolveResult> {
459
- const resolved = new Map<string, ResolvedRecipe>();
460
- const missingRepos = new Map<string, MissingRepo>();
461
- const missingManifests = new Set<string>();
462
- const cycles: string[][] = [];
463
- /** Which recipe pulled each recipe in, used to find a cycle's chain. */
464
- const parents = new Map<string, string>();
465
- /** Recipes whose manifest has already been walked, at the version noted. */
466
- const walked = new Map<string, string>();
467
-
468
- const queue: WorkItem[] = requests.map((request) => ({
469
- ...request,
470
- // A recipe ref that carries no range of its own takes the request's.
471
- ref:
472
- request.ref.kind === "recipe" && request.ref.range === undefined && request.range !== undefined
473
- ? { ...request.ref, range: request.range }
474
- : request.ref,
475
- kind: request.kind ?? "subscribes",
476
- }));
477
-
478
- while (queue.length > 0) {
479
- const item = queue.shift()!;
480
-
481
- // A ref naming a repository the project has not added never resolves and
482
- // never downloads anything; it comes back as a missing repository instead,
483
- // so the trust layer can ask about it by name.
484
- const qualifier = repoOf(item.ref)?.name;
485
- if (qualifier !== undefined && !Object.hasOwn(context.repos, qualifier)) {
486
- recordMissingRepo(missingRepos, {
487
- name: qualifier,
488
- requiredBy: [{ ref: formatRef(item.ref), requestedBy: item.requestedBy }],
489
- });
490
- continue;
491
- }
492
-
493
- // A dependency that named another repository by location is matched on that
494
- // location, so a project that already added it under some other short name
495
- // resolves against the copy it has. One it has not added carries its URL and
496
- // its provider, which is everything the trust round needs to offer to add it.
497
- if (item.remote !== undefined) {
498
- const added = repoNamedByIdentity(context.repos, item.remote.identity);
499
- if (added === undefined) {
500
- recordMissingRepo(missingRepos, {
501
- name: proposeRepoName(context.repos, item.remote.identity),
502
- url: item.remote.url,
503
- identity: item.remote.identity,
504
- ...(item.remote.provider === undefined ? {} : { provider: item.remote.provider }),
505
- requiredBy: [{ ref: item.remote.written, requestedBy: item.requestedBy }],
506
- });
507
- continue;
508
- }
509
- item.ref = withRepoName(item.ref, added);
510
- if (item.remote.browsed !== undefined) settleBrowsedItem(item, context.indexes.get(added));
511
- }
512
-
513
- if (item.ref.kind === "namespace") {
514
- queue.push(...expandNamespace(item, context));
515
- continue;
516
- }
517
-
518
- const recipe = resolveRecipeRef(item, context, resolved);
519
- resolved.set(recipe.key, recipe);
520
-
521
- if (item.requestedBy !== PROJECT_REQUESTER && !parents.has(recipe.key)) {
522
- parents.set(recipe.key, item.requestedBy);
523
- }
524
-
525
- // Walking a manifest is only worth doing once per version. A second holder
526
- // of an already-walked version adds itself and stops there; that is also
527
- // what keeps a dependency cycle finite.
528
- if (walked.get(recipe.key) === recipe.version) {
529
- const cycle = findCycle(recipe.key, item.requestedBy, parents);
530
- if (cycle !== undefined) cycles.push(cycle);
531
- continue;
532
- }
533
- walked.set(recipe.key, recipe.version);
534
-
535
- const lists = dependencyListsOf(recipe, await context.loadManifest(recipe));
536
- if (lists === undefined) {
537
- missingManifests.add(recipe.key);
538
- continue;
539
- }
540
-
541
- for (const [kind, refs] of [
542
- ["depends", lists.depends],
543
- ["subscribes", lists.subscribes],
544
- ] as Array<[LockKind, string[]]>) {
545
- for (const written of refs) {
546
- queue.push(dependencyRequest(written, recipe, kind, context));
547
- }
548
- }
549
- }
550
-
551
- // A recipe whose version was narrowed by a later holder has had TWO of its
552
- // versions walked, and the dependencies discovered from the version that was
553
- // replaced are still sitting in `resolved`. Walk the closure once more over
554
- // the versions actually settled on, and keep only what that reaches.
555
- await keepOnlyReachable(resolved, context);
556
-
557
- const ordered = [...resolved.values()].sort((left, right) =>
558
- left.key < right.key ? -1 : left.key > right.key ? 1 : 0
559
- );
560
- for (const recipe of ordered) recipe.requestedBy.sort();
561
-
562
- return {
563
- resolved: ordered,
564
- missingRepos: [...missingRepos.values()].sort((left, right) =>
565
- left.name < right.name ? -1 : 1
566
- ),
567
- missingManifests: [...missingManifests].sort(),
568
- cycles,
569
- };
570
- }
571
-
572
- /**
573
- * Drops every resolved recipe the settled closure no longer reaches, and trims
574
- * the holders and ranges of the ones that stay.
575
- *
576
- * The walk resolves refs in the order it meets them, so a recipe can be walked
577
- * at one version and then walked again at a lower one once a second holder
578
- * narrows its range. The lower version is what the closure settles on, but the
579
- * dependencies discovered from the higher one are already in `resolved`, held by
580
- * a parent that no longer declares them. Installing those is wrong twice over:
581
- * they are content nothing asked for, and the lockfile would record a holder
582
- * that does not hold them.
583
- *
584
- * So the closure is walked once more over the versions actually settled on. This
585
- * re-reads manifests that have already been read, which the loader serves from
586
- * the store; nothing is fetched. A recipe the loader cannot produce a manifest
587
- * for keeps everything it reached, since the alternative is dropping a
588
- * dependency because a manifest was unreadable.
589
- *
590
- * Versions are NOT re-picked. Trimming can only remove constraints, so the
591
- * version already chosen still satisfies every holder that remains; re-picking
592
- * could raise it and undo the narrowing that made this pass necessary.
593
- *
594
- * @param resolved - What the walk produced, edited in place.
595
- * @param context - The manifest loader.
596
- */
597
- async function keepOnlyReachable(
598
- resolved: Map<string, ResolvedRecipe>,
599
- context: ResolveContext
600
- ): Promise<void> {
601
- /** Who declares each key, and under which kind, in the settled closure. */
602
- const holders = new Map<string, Map<string, LockKind>>();
603
-
604
- /** Records one declaration, and reports whether the child is newly reached. */
605
- const declare = (parent: string, child: string, kind: LockKind): boolean => {
606
- const existing = holders.get(child);
607
- if (existing === undefined) {
608
- holders.set(child, new Map([[parent, kind]]));
609
- return true;
610
- }
611
- const previous = existing.get(parent);
612
- existing.set(parent, previous === "subscribes" || kind === "subscribes" ? "subscribes" : kind);
613
- return false;
614
- };
615
-
616
- const queue: string[] = [];
617
- for (const recipe of resolved.values()) {
618
- if (!recipe.requestedBy.includes(PROJECT_REQUESTER)) continue;
619
- if (declare(PROJECT_REQUESTER, recipe.key, "subscribes")) queue.push(recipe.key);
620
- }
621
-
622
- while (queue.length > 0) {
623
- const key = queue.shift()!;
624
- const recipe = resolved.get(key);
625
- if (recipe === undefined) continue;
626
-
627
- let manifest;
628
- try {
629
- manifest = await context.loadManifest(recipe);
630
- } catch {
631
- manifest = undefined;
632
- }
633
- const lists = dependencyListsOf(recipe, manifest);
634
-
635
- if (lists === undefined) {
636
- // An unreadable manifest is already reported as a missing manifest. Keep
637
- // everything this recipe held rather than dropping a dependency over it.
638
- for (const other of resolved.values()) {
639
- if (!other.requestedBy.includes(key)) continue;
640
- const kind = other.kind;
641
- if (declare(key, other.key, kind)) queue.push(other.key);
642
- }
643
- continue;
644
- }
645
-
646
- for (const [kind, refs] of [
647
- ["depends", lists.depends],
648
- ["subscribes", lists.subscribes],
649
- ] as Array<[LockKind, string[]]>) {
650
- for (const written of refs) {
651
- for (const target of dependencyTargets(written, recipe, context, resolved)) {
652
- if (!resolved.has(target)) continue;
653
- if (declare(key, target, kind)) queue.push(target);
654
- }
655
- }
656
- }
657
- }
658
-
659
- for (const [key, recipe] of [...resolved]) {
660
- const reached = holders.get(key);
661
- if (reached === undefined) {
662
- resolved.delete(key);
663
- continue;
664
- }
665
- recipe.requestedBy = [...reached.keys()];
666
- recipe.ranges = recipe.ranges.filter((entry) => reached.has(entry.requestedBy));
667
- recipe.kind = [...reached.values()].includes("subscribes") ? "subscribes" : "depends";
668
- }
669
- }
670
-
671
- /**
672
- * The resolved keys one dependency of a recipe reaches, the same way the walk
673
- * reached them: a namespace means every recipe in it, and a browsed path means
674
- * the recipe it settles on. A dependency that no longer settles reaches
675
- * nothing, because the walk would have failed on it.
676
- *
677
- * @param written - The dependency as the manifest wrote it.
678
- * @param holder - The recipe that declared it.
679
- * @param context - The added repositories and their cached indexes.
680
- * @param resolved - What the walk resolved.
681
- */
682
- function dependencyTargets(
683
- written: string,
684
- holder: ResolvedRecipe,
685
- context: ResolveContext,
686
- resolved: Map<string, ResolvedRecipe>
687
- ): string[] {
688
- let item: WorkItem;
689
- try {
690
- item = dependencyRequest(written, holder, "depends", context);
691
- if (item.remote?.browsed !== undefined) {
692
- const added = repoNamedByIdentity(context.repos, item.remote.identity);
693
- settleBrowsedItem(item, added === undefined ? undefined : context.indexes.get(added));
694
- }
695
- } catch {
696
- return [];
697
- }
698
- const namespace = namespaceOf(item.ref)!.name;
699
- return item.ref.kind === "namespace"
700
- ? [...resolved.keys()].filter((entry) => entry.startsWith(`${namespace}/`))
701
- : [shortKey(item.ref)];
702
- }
703
-
704
- /**
705
- * What a resolved recipe declares under `depends` and `subscribes`: its
706
- * manifest's lists when the manifest could be read, and otherwise the lists its
707
- * index entry records, or undefined when neither says.
708
- *
709
- * @param recipe - The resolved recipe, carrying the lists its index entry records.
710
- * @param manifest - Its manifest, when the loader produced one.
711
- */
712
- function dependencyListsOf(
713
- recipe: ResolvedRecipe,
714
- manifest: RecipeManifest | undefined
715
- ): DependencyLists | undefined {
716
- if (manifest !== undefined) {
717
- return { depends: manifest.depends ?? [], subscribes: manifest.subscribes ?? [] };
718
- }
719
- return recipe.declares;
720
- }
721
-
722
- /** Records one more reason a repository is needed. */
723
- function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo): void {
724
- const existing = into.get(missing.name);
725
- if (existing === undefined) {
726
- into.set(missing.name, missing);
727
- return;
728
- }
729
- for (const entry of missing.requiredBy) {
730
- const duplicate = existing.requiredBy.some(
731
- (known) => known.ref === entry.ref && known.requestedBy === entry.requestedBy
732
- );
733
- if (!duplicate) existing.requiredBy.push(entry);
734
- }
735
- }
736
-
737
- /**
738
- * Expands a namespace ref into one request per recipe in that namespace. A
739
- * namespace subscription means "everything in here, including whatever is added
740
- * later", so the expansion happens fresh on every resolution.
741
- *
742
- * @param item - The namespace request.
743
- * @param context - The cached indexes and added repositories.
744
- */
745
- function expandNamespace(item: WorkItem, context: ResolveContext): WorkItem[] {
746
- const namespace = namespaceOf(item.ref)!.name;
747
- const qualifier = repoOf(item.ref)?.name;
748
- const requestedRange = item.range;
749
- const expanded: WorkItem[] = [];
750
- const seen = new Set<string>();
751
- let namespaceFound = false;
752
-
753
- for (const [repoName, index] of context.indexes) {
754
- if (qualifier !== undefined && repoName !== qualifier) continue;
755
- if (!Object.hasOwn(index.namespaces, namespace)) continue;
756
- namespaceFound = true;
757
-
758
- for (const key of Object.keys(index.recipes)) {
759
- if (!key.startsWith(`${namespace}/`)) continue;
760
- if (seen.has(key)) continue;
761
- seen.add(key);
762
- expanded.push({
763
- // A namespace ref cannot carry a range when it is WRITTEN, but a
764
- // namespace subscription entry can carry one, and a caller builds the
765
- // request from that entry. When it does, the range applies to every
766
- // recipe in the namespace; that is how the built-in `core`
767
- // subscription stays pinned to the running sous version.
768
- ref: recipeRef(namespace, key.slice(namespace.length + 1), {
769
- ...(qualifier === undefined ? {} : { repo: qualifier }),
770
- ...(requestedRange === undefined ? {} : { range: requestedRange }),
771
- }),
772
- requestedBy: item.requestedBy,
773
- kind: item.kind,
774
- ...(item.prerelease === undefined ? {} : { prerelease: item.prerelease }),
775
- });
776
- }
777
- }
778
-
779
- if (!namespaceFound) throw unknownRefError(item, context, "namespace");
780
-
781
- return expanded;
782
- }
783
-
784
- /**
785
- * Resolves one recipe ref against every added repository, merging it with
786
- * anything already resolved for the same recipe key.
787
- *
788
- * @param item - The request being resolved.
789
- * @param context - The cached indexes and added repositories.
790
- * @param resolved - What is resolved so far, so ranges accumulate.
791
- */
792
- function resolveRecipeRef(
793
- item: WorkItem,
794
- context: ResolveContext,
795
- resolved: Map<string, ResolvedRecipe>
796
- ): ResolvedRecipe {
797
- const key = shortKey(item.ref);
798
- const qualifier = repoOf(item.ref)?.name;
799
-
800
- const candidates: Array<{ repo: string; index: IndexFile }> = [];
801
- for (const [repoName, index] of context.indexes) {
802
- if (qualifier !== undefined && repoName !== qualifier) continue;
803
- if (Object.hasOwn(index.recipes, key)) candidates.push({ repo: repoName, index });
804
- }
805
-
806
- if (candidates.length === 0) throw unknownRefError(item, context, "recipe");
807
- if (candidates.length > 1) throw ambiguousRefError(item, candidates.map((c) => c.repo));
808
-
809
- const chosen = candidates[0]!;
810
- const entry = chosen.index.recipes[key]!;
811
- const previous = resolved.get(key);
812
-
813
- if (previous !== undefined && previous.repo !== chosen.repo) {
814
- throw new ConfigError(
815
- `The recipe '${key}' is being taken from two different repositories at once: ` +
816
- `'${previous.repo}' and '${chosen.repo}'.\n` +
817
- ` Qualify the refs that ask for it, so each one says which repository it means.`
818
- );
819
- }
820
-
821
- const ranges = [...(previous?.ranges ?? [])];
822
- const range = rangeOf(item.ref) ?? "*";
823
- if (!ranges.some((known) => known.range === range && known.requestedBy === item.requestedBy)) {
824
- ranges.push({ range, requestedBy: item.requestedBy });
825
- }
826
-
827
- const prerelease =
828
- (previous?.prerelease ?? false) ||
829
- (item.prerelease ?? context.prerelease ?? false);
830
-
831
- const version = pickVersion(key, entry, ranges, prerelease, context.keep?.[key]);
832
- const versionEntry = entry.versions[version]!;
833
- const declares = indexDependencyLists(versionEntry);
834
-
835
- const requestedBy = [...(previous?.requestedBy ?? [])];
836
- if (!requestedBy.includes(item.requestedBy)) requestedBy.push(item.requestedBy);
837
-
838
- const { namespace, name } = splitRecipeKey(key);
839
- return {
840
- key,
841
- namespace,
842
- name,
843
- repo: chosen.repo,
844
- identity: context.repos[chosen.repo]?.identity ?? chosen.repo,
845
- version,
846
- hash: versionEntry.hash,
847
- tag: versionEntry.tag,
848
- path: entry.path,
849
- ...(versionEntry.dependencies === undefined
850
- ? {}
851
- : { dependencies: versionEntry.dependencies }),
852
- ...(versionEntry.variables === undefined ? {} : { variables: versionEntry.variables }),
853
- ...(declares === undefined ? {} : { declares }),
854
- // A recipe held as a co-subscription by anyone is a co-subscription; a
855
- // build dependency only stays one while nothing subscribes to it.
856
- kind: previous?.kind === "subscribes" || item.kind === "subscribes" ? "subscribes" : "depends",
857
- requestedBy,
858
- ranges,
859
- prerelease,
860
- };
861
- }
862
-
863
- /**
864
- * Picks the highest published version satisfying every range at once.
865
- * Prereleases stay out of the match unless they were opted into.
866
- *
867
- * @param key - The recipe key, for error messages.
868
- * @param entry - The recipe's index entry.
869
- * @param ranges - Every range that has to hold, with who asked for it.
870
- * @param prerelease - Whether prereleases may match.
871
- * @param keep - A version to hold, chosen whenever it is among the candidates.
872
- */
873
- function pickVersion(
874
- key: string,
875
- entry: IndexFile["recipes"][string],
876
- ranges: Array<{ range: string; requestedBy: string }>,
877
- prerelease: boolean,
878
- keep?: string
879
- ): string {
880
- const published = Object.keys(entry.versions);
881
- const eligible = prerelease
882
- ? published
883
- : published.filter((version) => entry.versions[version]!.prerelease !== true);
884
-
885
- let candidates = eligible;
886
- for (const { range } of ranges) {
887
- candidates = candidates.filter((version) =>
888
- semver.satisfies(version, range, { includePrerelease: prerelease })
889
- );
890
- }
891
-
892
- // A held version is kept exactly as long as every range still allows it. It
893
- // is checked against the published list rather than the eligible one, so a
894
- // prerelease a subscription once opted into is not moved just for being one.
895
- if (
896
- keep !== undefined &&
897
- Object.hasOwn(entry.versions, keep) &&
898
- ranges.every(({ range }) =>
899
- semver.satisfies(keep, range, { includePrerelease: true })
900
- )
901
- ) {
902
- return keep;
903
- }
904
-
905
- const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
906
- if (best !== null) return best;
907
-
908
- const asked = ranges
909
- .map(({ range, requestedBy }) => ` ${range} (required by ${requestedBy})`)
910
- .join("\n");
911
- const available = sortVersions(published).join(", ");
912
- const prereleaseNote = prerelease
913
- ? ""
914
- : "\n Prerelease versions were not considered. A subscription may opt into them " +
915
- "with 'prerelease: true', and 'sous subscribe' with '--prerelease'.";
916
-
917
- throw new ConfigError(
918
- `No published version of '${key}' satisfies what was asked for.\n` +
919
- ` Version ${ranges.length === 1 ? "range" : "ranges"} asked for:\n${asked}\n` +
920
- ` Versions this repository publishes: ${available}.${prereleaseNote}`
921
- );
922
- }
923
-
924
- /** Sorts version strings newest first, leaving anything unparsable at the end. */
925
- function sortVersions(versions: string[]): string[] {
926
- return [...versions].sort((left, right) => semver.rcompare(left, right, { loose: true }));
927
- }
928
-
929
- /**
930
- * Builds the error for a ref that no added repository publishes. It names the
931
- * repositories that were searched, because "not found" almost always means
932
- * "the repository publishing it has not been added yet".
933
- *
934
- * @param item - The request that could not be resolved.
935
- * @param context - The cached indexes and added repositories.
936
- * @param what - Whether a namespace or a recipe was being looked for.
937
- */
938
- function unknownRefError(
939
- item: WorkItem,
940
- context: ResolveContext,
941
- what: "namespace" | "recipe"
942
- ): ConfigError {
943
- const written = item.remote?.written ?? formatRef(item.ref);
944
- const searched = [...context.indexes.keys()];
945
- const where =
946
- searched.length === 0
947
- ? " This project has added no repositories yet."
948
- : ` Repositories searched: ${searched.join(", ")}.`;
949
- const asker =
950
- item.requestedBy === PROJECT_REQUESTER
951
- ? ""
952
- : `\n It was required by the recipe '${item.requestedBy}'.`;
953
-
954
- // When the ref said WHICH repository, the useful answer is what that
955
- // repository does publish: a dependency that names something it has never
956
- // heard of is almost always a typo or a recipe that was renamed.
957
- const named = repoOf(item.ref)?.name;
958
- const index = named === undefined ? undefined : context.indexes.get(named);
959
- const publishes =
960
- index === undefined
961
- ? ""
962
- : `\n The repository '${named}' publishes: ${
963
- Object.keys(index.recipes).length === 0
964
- ? "nothing yet"
965
- : Object.keys(index.recipes).sort().join(", ")
966
- }.`;
967
-
968
- return new ConfigError(
969
- `No added repository publishes the ${what} '${written}'.\n` +
970
- `${where}${asker}${publishes}\n` +
971
- ` Add the repository that publishes it with 'sous repo add <url>', then try again.`
972
- );
973
- }
974
-
975
- /**
976
- * Builds the error for a ref that resolves in more than one repository. Sous
977
- * never picks a winner; it shows the qualified form for each repository and
978
- * asks which one was meant.
979
- *
980
- * @param item - The ambiguous request.
981
- * @param repos - The repositories that publish it.
982
- */
983
- function ambiguousRefError(item: WorkItem, repos: string[]): ConfigError {
984
- const key = shortKey(item.ref);
985
- const itemRange = rangeOf(item.ref);
986
- const range = itemRange === undefined ? "" : `@${itemRange}`;
987
- const qualified = repos.map((repo) => ` ${repo}:${key}${range}`).join("\n");
988
- const asker =
989
- item.requestedBy === PROJECT_REQUESTER
990
- ? ""
991
- : ` It was required by the recipe '${item.requestedBy}'.`;
992
-
993
- return new ConfigError(
994
- `The ref '${key}' is published by more than one added repository: ${repos.join(", ")}.` +
995
- `${asker}\n` +
996
- ` Say which repository you mean by qualifying the ref with its name:\n${qualified}`
997
- );
998
- }
999
-
1000
- /**
1001
- * Finds the chain of recipes leading from a recipe back to itself, or undefined
1002
- * when there is none. A cycle is reported rather than treated as an error: the
1003
- * closure is still finite, and two recipes that co-subscribe to each other are
1004
- * unusual but not broken.
1005
- *
1006
- * @param key - The recipe reached a second time.
1007
- * @param from - The recipe that reached it.
1008
- * @param parents - Who pulled each recipe in.
1009
- */
1010
- function findCycle(
1011
- key: string,
1012
- from: string,
1013
- parents: Map<string, string>
1014
- ): string[] | undefined {
1015
- if (from === PROJECT_REQUESTER) return undefined;
1016
-
1017
- const chain = [key];
1018
- let current: string | undefined = from;
1019
- const guard = new Set<string>();
1020
- while (current !== undefined && current !== PROJECT_REQUESTER) {
1021
- chain.push(current);
1022
- if (current === key) return chain.reverse();
1023
- if (guard.has(current)) return undefined;
1024
- guard.add(current);
1025
- current = parents.get(current);
1026
- }
1027
- return undefined;
1028
- }