@sous-io/sous 0.2.32 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/docs/markdown/_sidebar.md +1 -0
- package/docs/markdown/commands.md +8 -4
- package/docs/markdown/configuration.md +28 -4
- package/docs/markdown/repositories-authoring.md +22 -2
- package/docs/markdown/repositories-consuming.md +40 -11
- package/docs/markdown/repositories-file-formats.md +28 -13
- package/docs/markdown/repositories-quickstart.md +1 -1
- package/docs/markdown/repositories-variables.md +16 -0
- package/docs/markdown/repositories.md +8 -4
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +27 -15
- package/src/commands/lock/rebuild.ts +3 -6
- package/src/commands/namespace/show.ts +2 -2
- package/src/commands/recipe/show.ts +26 -9
- package/src/commands/repo/contribute.ts +32 -23
- package/src/commands/repo/gc.ts +1 -1
- package/src/commands/repo/remove.ts +1 -1
- package/src/commands/repo/search.ts +1 -1
- package/src/commands/vars/ask.ts +75 -54
- package/src/lib/build-service.ts +93 -15
- package/src/lib/config-schema.ts +59 -19
- package/src/lib/include-resolver.ts +307 -69
- package/src/lib/markdown-compiler.ts +289 -82
- package/src/lib/project-scaffold/templates.ts +19 -11
- package/src/lib/repos/catalog-inputs.ts +1 -1
- package/src/lib/repos/catalog.ts +98 -72
- package/src/lib/repos/declarations.ts +22 -12
- package/src/lib/repos/formats/index-file.ts +1 -1
- package/src/lib/repos/formats/recipe-manifest.ts +3 -4
- package/src/lib/repos/lock-service.ts +1 -1
- package/src/lib/repos/locked-namespace-resolver.ts +14 -6
- package/src/lib/repos/locked-recipe-lookup.ts +123 -0
- package/src/lib/repos/locked-recipes.ts +1 -1
- package/src/lib/repos/memories-hash-name.ts +33 -0
- package/src/lib/repos/namespace-resolver.ts +198 -119
- package/src/lib/repos/recipe-config-layers.ts +29 -1
- package/src/lib/repos/recipe-key-matcher.ts +62 -0
- package/src/lib/repos/recipe-memories.ts +175 -0
- package/src/lib/repos/recipe-targets.ts +32 -28
- package/src/lib/repos/ref-search.ts +1 -1
- package/src/lib/repos/release/index-builder.ts +23 -15
- package/src/lib/repos/release/plan.ts +14 -7
- package/src/lib/repos/release/settle.ts +38 -121
- package/src/lib/repos/release/submit-checkout.ts +42 -23
- package/src/lib/repos/release/tags.ts +1 -1
- package/src/lib/repos/resolver.ts +90 -72
- package/src/lib/repos/subscription-service.ts +192 -170
- package/src/lib/settings.ts +37 -33
- package/src/lib/vars/answers.ts +9 -1
- package/src/lib/vars/ask.ts +3 -3
- package/src/lib/vars/display.ts +1 -3
- package/src/lib/vars/index.ts +1 -0
- package/src/lib/vars/mappings.ts +17 -9
- package/src/lib/vars/secrets.ts +131 -0
- package/src/services/ref-resolver/container.ts +107 -0
- package/src/services/ref-resolver/dependency.ts +60 -0
- package/src/services/ref-resolver/format.ts +197 -0
- package/src/services/ref-resolver/glob.ts +115 -0
- package/src/services/ref-resolver/hash-names.ts +155 -0
- package/src/services/ref-resolver/index.ts +55 -0
- package/src/services/ref-resolver/injectable.ts +36 -0
- package/src/services/ref-resolver/location.ts +62 -0
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
- package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
- package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
- package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
- package/src/services/ref-resolver/named-refs.ts +144 -0
- package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
- package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
- package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
- package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
- package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
- package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
- package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
- package/src/services/ref-resolver/parts.ts +115 -0
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
- package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
- package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
- package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
- package/src/services/ref-resolver/ref-helpers.ts +62 -0
- package/src/services/ref-resolver/ref-picker-service.ts +204 -0
- package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
- package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
- package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
- package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
- package/src/services/ref-resolver/source.ts +48 -0
- package/src/services/ref-resolver/tokens.ts +25 -0
- package/src/services/ref-resolver/types.ts +155 -0
- package/src/templating/init-liquid-engine.ts +33 -18
- package/src/templating/lib/secret-scope.ts +41 -0
- package/src/templating/tags/exportScalarVarsJs.ts +5 -2
- package/src/templating/tags/showVars.ts +8 -2
- package/src/lib/refs/find.ts +0 -770
- package/src/lib/refs/index.ts +0 -14
- package/src/lib/refs/parse.ts +0 -844
- package/src/lib/refs/pick.ts +0 -147
- package/src/lib/refs/scopes.ts +0 -95
- package/src/lib/refs/settle.ts +0 -219
package/src/lib/refs/parse.ts
DELETED
|
@@ -1,844 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ref parser: the one place a written ref becomes the things it could mean.
|
|
3
|
-
*
|
|
4
|
-
* A "ref" is how everything in the Repositories system names a namespace or a
|
|
5
|
-
* recipe: on the command line, in a project's subscriptions, in a recipe
|
|
6
|
-
* manifest's `depends` and `subscribes` lists, and in the keys sous stores. One
|
|
7
|
-
* function reads all of them, and is told where the ref came from:
|
|
8
|
-
*
|
|
9
|
-
* parseRef(ref, from = RefSource.CommandLine)
|
|
10
|
-
*
|
|
11
|
-
* It recognizes every form a person might write:
|
|
12
|
-
*
|
|
13
|
-
* workflow a namespace
|
|
14
|
-
* workflow/alpha a recipe
|
|
15
|
-
* workflow/* a namespace, spelled out
|
|
16
|
-
* sous-recipes:workflow/alpha a recipe in a named repository
|
|
17
|
-
* workflow/alpha@^1.2.0 any of these with a version range
|
|
18
|
-
* github://owner/repo/workflow a namespace, by location
|
|
19
|
-
* github://owner/repo/workflow/* the same, spelled out
|
|
20
|
-
* github://owner/repo/workflow/alpha a recipe, by location
|
|
21
|
-
* https://github.com/owner/repo/workflow/alpha an HTTPS URL
|
|
22
|
-
* github.com/owner/repo/workflow/alpha a URL with no scheme
|
|
23
|
-
* git@github.com:owner/repo.git an SSH remote
|
|
24
|
-
* https://github.com/owner/repo/tree/main/recipes/workflow/alpha
|
|
25
|
-
* a browser URL from the file view
|
|
26
|
-
* https://gitlab.com/group/sub/project/-/tree/main/recipes/workflow/alpha
|
|
27
|
-
* GitLab, nested groups included
|
|
28
|
-
*
|
|
29
|
-
* and returns EVERY reading of it, because some forms can be read more than one
|
|
30
|
-
* way: a GitLab URL with nested groups does not say where the project path
|
|
31
|
-
* ends, and a browser URL names a folder whose recipe only the repository's
|
|
32
|
-
* index can say. Whoever holds the index settles them (`settle.ts`).
|
|
33
|
-
*
|
|
34
|
-
* The place (`from`) decides which forms are allowed; a refused form is an
|
|
35
|
-
* error saying what to write there instead. Nothing host-specific lives here:
|
|
36
|
-
* where a repository path ends and what a browser path looks like are each
|
|
37
|
-
* provider's own rule, asked through `RepoProvider.readLocation`.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import semver from "semver";
|
|
41
|
-
import { ConfigError } from "../errors.js";
|
|
42
|
-
import { repoIdentity } from "../repos/identity.js";
|
|
43
|
-
import {
|
|
44
|
-
builtInProviders,
|
|
45
|
-
detectProvider,
|
|
46
|
-
providerById,
|
|
47
|
-
type ProviderId,
|
|
48
|
-
type RepoProvider,
|
|
49
|
-
} from "../repos/providers/index.js";
|
|
50
|
-
import { RefSource } from "./scopes.js";
|
|
51
|
-
|
|
52
|
-
// --- What a reading is ----------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/** Where a located ref says its repository lives. */
|
|
55
|
-
export type RefLocation = {
|
|
56
|
-
/** The provider that reads the location. */
|
|
57
|
-
provider: ProviderId;
|
|
58
|
-
/** The host, such as `github.com`. */
|
|
59
|
-
host: string;
|
|
60
|
-
/** The repository's path on the host, such as `sous-io/sous-recipes`. */
|
|
61
|
-
repoPath: string;
|
|
62
|
-
/** The repository's canonical identity, as the store and the lockfile key it. */
|
|
63
|
-
identity: string;
|
|
64
|
-
/** The repository's HTTPS URL, which is what adding it is handed. */
|
|
65
|
-
url: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* A reading that names a namespace or a recipe. `recipe` is absent for a
|
|
70
|
-
* namespace; `range` needs a recipe.
|
|
71
|
-
*/
|
|
72
|
-
export type ParsedRef = {
|
|
73
|
-
/** The repository short name from a `repo:` qualifier, when one was given. */
|
|
74
|
-
repo?: string;
|
|
75
|
-
/** Where the repository lives, when the ref named it by location. */
|
|
76
|
-
location?: RefLocation;
|
|
77
|
-
/** The namespace. */
|
|
78
|
-
namespace: string;
|
|
79
|
-
/** The recipe name, when the ref names a recipe rather than a whole namespace. */
|
|
80
|
-
recipe?: string;
|
|
81
|
-
/** The semantic version range, when one was given. Only legal with a recipe. */
|
|
82
|
-
range?: string;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* A reading that names a folder in a repository, copied from a host's file
|
|
87
|
-
* view. Which recipe or namespace that is only the repository's index can say,
|
|
88
|
-
* through the `path` each recipe's entry records.
|
|
89
|
-
*/
|
|
90
|
-
export type BrowsedRef = {
|
|
91
|
-
/** Where the repository lives. */
|
|
92
|
-
location: RefLocation;
|
|
93
|
-
/** The path after `tree/` or `blob/`, with the branch still in front of it. */
|
|
94
|
-
browsed: string;
|
|
95
|
-
/** The version range, when one was given. */
|
|
96
|
-
range?: string;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/** A reading that names a whole repository and nothing inside it. */
|
|
100
|
-
export type RepositoryRef = {
|
|
101
|
-
/** Where the repository lives. */
|
|
102
|
-
location: RefLocation;
|
|
103
|
-
/** Always true; tells this reading apart from the others. */
|
|
104
|
-
repository: true;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** One thing a written ref could mean. */
|
|
108
|
-
export type RefReading = ParsedRef | BrowsedRef | RepositoryRef;
|
|
109
|
-
|
|
110
|
-
/** True when a reading names a namespace or a recipe. */
|
|
111
|
-
export function isNamedReading(reading: RefReading): reading is ParsedRef {
|
|
112
|
-
return "namespace" in reading;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** True when a reading names a browser path still to be settled. */
|
|
116
|
-
export function isBrowsedReading(reading: RefReading): reading is BrowsedRef {
|
|
117
|
-
return "browsed" in reading;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** True when a reading names a whole repository. */
|
|
121
|
-
export function isRepositoryReading(reading: RefReading): reading is RepositoryRef {
|
|
122
|
-
return "repository" in reading;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// --- The parser -----------------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
/** What each place is called in a sentence. */
|
|
128
|
-
const PLACE_LABELS: Record<RefSource, string> = {
|
|
129
|
-
[RefSource.CommandLine]: "on the command line",
|
|
130
|
-
[RefSource.Config]: "as a subscription key in a config file",
|
|
131
|
-
[RefSource.Manifest]: "in a recipe manifest's 'depends' or 'subscribes' list",
|
|
132
|
-
[RefSource.Lockfile]: "as a key sous stores",
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/** The forms each place allows, for the reminder under a ref that does not parse. */
|
|
136
|
-
const SYNTAX_HELP: Record<RefSource, string> = {
|
|
137
|
-
[RefSource.CommandLine]:
|
|
138
|
-
"A ref is written as 'namespace', 'namespace/recipe', 'repo:namespace/recipe', " +
|
|
139
|
-
"any of those with '@<range>', or a location such as " +
|
|
140
|
-
"'github://owner/repository/namespace/recipe' or a URL copied from the browser.",
|
|
141
|
-
[RefSource.Config]:
|
|
142
|
-
"A subscription key is written as 'namespace' or 'namespace/recipe'.",
|
|
143
|
-
[RefSource.Manifest]:
|
|
144
|
-
"A dependency is written either as a bare ref naming a recipe in this same repository " +
|
|
145
|
-
"('workflow/task-files', optionally with a range such as 'workflow/task-files@^1.1'), " +
|
|
146
|
-
"or by location for one in another repository " +
|
|
147
|
-
"('github://sous-io/sous-recipes/workflow/task-files@^1.1', an HTTPS or SSH URL, or a " +
|
|
148
|
-
"URL copied from the browser).",
|
|
149
|
-
[RefSource.Lockfile]: "A stored key is written as 'namespace' or 'namespace/recipe'.",
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/** A name as the parser accepts it: kebab-case, in any case. */
|
|
153
|
-
const NAME_ANY_CASE = /^[a-z][a-z0-9-]*$/i;
|
|
154
|
-
|
|
155
|
-
/** A name as it is stored: lowercase kebab-case. */
|
|
156
|
-
const NAME_STORED = /^[a-z][a-z0-9-]*$/;
|
|
157
|
-
|
|
158
|
-
/** A URL that names its scheme, such as `https://` or `github://`. */
|
|
159
|
-
const SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/(.*)$/is;
|
|
160
|
-
|
|
161
|
-
/** The `scp`-style SSH form, as in `git@github.com:owner/name.git`. */
|
|
162
|
-
const SCP_PATTERN = /^[^@\s/:]+@([^@\s/:]+):(.+)$/s;
|
|
163
|
-
|
|
164
|
-
/** Schemes that are ordinary URLs rather than a provider's own identifier. */
|
|
165
|
-
const URL_SCHEMES = new Set(["http", "https", "ssh", "git", "git+ssh", "ssh+git"]);
|
|
166
|
-
|
|
167
|
-
/** Schemes that name a repository on this machine. */
|
|
168
|
-
const LOCAL_SCHEMES = new Set(["local", "file"]);
|
|
169
|
-
|
|
170
|
-
/** The reason one reading of a location was left out, kept for the error. */
|
|
171
|
-
type Dropped = { reason: string };
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Every reading of a written ref, allowed where it was written.
|
|
175
|
-
*
|
|
176
|
-
* parseRef("workflow/alpha@^1.2");
|
|
177
|
-
* // -> [{ namespace: "workflow", recipe: "alpha", range: "^1.2" }]
|
|
178
|
-
*
|
|
179
|
-
* parseRef("https://gitlab.com/a/b/c/d", RefSource.Manifest);
|
|
180
|
-
* // -> [{ location: gitlab.com/a/b, namespace: "c", recipe: "d" },
|
|
181
|
-
* // { location: gitlab.com/a/b/c, namespace: "d" }]
|
|
182
|
-
*
|
|
183
|
-
* @param input - The ref exactly as it was written.
|
|
184
|
-
* @param from - Where it was written, which decides the forms allowed.
|
|
185
|
-
* @param providers - The providers that read locations. Defaults to the built-ins.
|
|
186
|
-
* @returns Every reading, never empty. A ref that fits no form, or a form the
|
|
187
|
-
* place refuses, raises a ConfigError instead.
|
|
188
|
-
*/
|
|
189
|
-
export function parseRef(
|
|
190
|
-
input: string,
|
|
191
|
-
from: RefSource = RefSource.CommandLine,
|
|
192
|
-
providers: RepoProvider[] = builtInProviders()
|
|
193
|
-
): RefReading[] {
|
|
194
|
-
if (typeof input !== "string") {
|
|
195
|
-
throw refError(String(input), from, "a ref must be a string.");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const trimmed = input.trim();
|
|
199
|
-
if (trimmed.length === 0) throw refError(input, from, "a ref must not be empty.");
|
|
200
|
-
|
|
201
|
-
if (trimmed.startsWith("@")) {
|
|
202
|
-
throw refError(
|
|
203
|
-
input,
|
|
204
|
-
from,
|
|
205
|
-
"refs take no '@' prefix. The '@' character introduces a version range only, " +
|
|
206
|
-
"as in 'workflow/task-files@^1.2.0'."
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (trimmed.startsWith("~")) {
|
|
210
|
-
throw refError(
|
|
211
|
-
input,
|
|
212
|
-
from,
|
|
213
|
-
"refs take no '~' prefix. The '~' sigil belongs to template include lines " +
|
|
214
|
-
"('@~workflow/file.md'); a ref itself is written without it."
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const readings = looksLikeLocation(trimmed)
|
|
219
|
-
? readLocated(input, trimmed, from, providers)
|
|
220
|
-
: [readShort(input, trimmed, from)];
|
|
221
|
-
|
|
222
|
-
return readings.map((reading) => storedCase(reading, from));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* True when a written ref names a location (a URL, an SSH remote, a
|
|
227
|
-
* provider-scheme locator or a host path) rather than a short name. A short
|
|
228
|
-
* name never carries `://`, an `@` before a `:`, or a dot in its first segment.
|
|
229
|
-
*
|
|
230
|
-
* @param ref - The ref as written.
|
|
231
|
-
*/
|
|
232
|
-
export function looksLikeLocation(ref: string): boolean {
|
|
233
|
-
const trimmed = ref.trim();
|
|
234
|
-
if (SCHEME_PATTERN.test(trimmed)) return true;
|
|
235
|
-
if (SCP_PATTERN.test(trimmed)) return true;
|
|
236
|
-
const first = trimmed.split("/")[0] ?? "";
|
|
237
|
-
return trimmed.includes("/") && first.includes(".") && !first.includes(":");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The one reading of a ref that must name a namespace or a recipe by its short
|
|
242
|
-
* form: a config key, a stored key, or a ref a caller has already settled.
|
|
243
|
-
*
|
|
244
|
-
* @param input - The ref as written.
|
|
245
|
-
* @param from - Where it was written.
|
|
246
|
-
*/
|
|
247
|
-
export function parseShortRef(input: string, from: RefSource = RefSource.CommandLine): ParsedRef {
|
|
248
|
-
const readings = parseRef(input, from);
|
|
249
|
-
const only = readings[0]!;
|
|
250
|
-
if (readings.length === 1 && isNamedReading(only) && only.location === undefined) return only;
|
|
251
|
-
throw refError(
|
|
252
|
-
input,
|
|
253
|
-
from,
|
|
254
|
-
"this needs a namespace or a recipe named by its short form, such as 'workflow/alpha', " +
|
|
255
|
-
"and this ref names a location."
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The namespace and name of a stored recipe key, `namespace/recipe`.
|
|
261
|
-
*
|
|
262
|
-
* splitRecipeKey("workflow/alpha"); // -> { namespace: "workflow", name: "alpha" }
|
|
263
|
-
*
|
|
264
|
-
* @param key - A key sous stored.
|
|
265
|
-
*/
|
|
266
|
-
export function splitRecipeKey(key: string): { namespace: string; name: string } {
|
|
267
|
-
const parsed = parseShortRef(key, RefSource.Lockfile);
|
|
268
|
-
if (parsed.recipe === undefined) {
|
|
269
|
-
throw refError(key, RefSource.Lockfile, "a recipe key names a namespace and a recipe.");
|
|
270
|
-
}
|
|
271
|
-
return { namespace: parsed.namespace, name: parsed.recipe };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* The namespace a stored key belongs to: the key itself for a namespace, and
|
|
276
|
-
* its namespace for a recipe.
|
|
277
|
-
*
|
|
278
|
-
* namespaceOfKey("workflow/alpha"); // -> "workflow"
|
|
279
|
-
*
|
|
280
|
-
* @param key - A key sous stored.
|
|
281
|
-
*/
|
|
282
|
-
export function namespaceOfKey(key: string): string {
|
|
283
|
-
return parseShortRef(key, RefSource.Lockfile).namespace;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// --- Formatting -----------------------------------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* The ref's identity, with the repository and the version range dropped:
|
|
290
|
-
* `namespace` for a namespace, `namespace/recipe` for a recipe. It is the key
|
|
291
|
-
* everything is stored under (subscriptions, the index, the lockfile), so the
|
|
292
|
-
* same recipe is never recorded twice under two spellings.
|
|
293
|
-
*
|
|
294
|
-
* @param parsed - The reading.
|
|
295
|
-
*/
|
|
296
|
-
export function refKey(parsed: ParsedRef): string {
|
|
297
|
-
return parsed.recipe === undefined ? parsed.namespace : `${parsed.namespace}/${parsed.recipe}`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** True when the reading names a whole namespace rather than a single recipe. */
|
|
301
|
-
export function isNamespaceRef(parsed: ParsedRef): boolean {
|
|
302
|
-
return parsed.recipe === undefined;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* The canonical written form of a reading, which is how sous prints it: the
|
|
307
|
-
* short form for a short ref, and each provider's own canonical locator for a
|
|
308
|
-
* located one.
|
|
309
|
-
*
|
|
310
|
-
* formatRef({ repo: "sous-recipes", namespace: "workflow", recipe: "alpha" });
|
|
311
|
-
* // -> "sous-recipes:workflow/alpha"
|
|
312
|
-
*
|
|
313
|
-
* @param reading - The reading.
|
|
314
|
-
* @param providers - The providers that format locators. Defaults to the built-ins.
|
|
315
|
-
*/
|
|
316
|
-
export function formatRef(
|
|
317
|
-
reading: RefReading,
|
|
318
|
-
providers: RepoProvider[] = builtInProviders()
|
|
319
|
-
): string {
|
|
320
|
-
const range = "range" in reading && reading.range !== undefined ? `@${reading.range}` : "";
|
|
321
|
-
|
|
322
|
-
if (reading.location === undefined) {
|
|
323
|
-
const named = reading as ParsedRef;
|
|
324
|
-
const qualifier = named.repo === undefined ? "" : `${named.repo}:`;
|
|
325
|
-
return `${qualifier}${refKey(named)}${range}`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const { host, repoPath } = reading.location;
|
|
329
|
-
const provider = providerById(reading.location.provider, providers);
|
|
330
|
-
const rest = isNamedReading(reading)
|
|
331
|
-
? refKey(reading)
|
|
332
|
-
: isBrowsedReading(reading)
|
|
333
|
-
? `tree/${reading.browsed}`
|
|
334
|
-
: "";
|
|
335
|
-
/* c8 ignore next 2 */
|
|
336
|
-
if (provider === undefined) return `${reading.location.url}/${rest}${range}`;
|
|
337
|
-
return `${provider.formatLocator(host, repoPath, rest)}${range}`;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// --- Short refs -----------------------------------------------------------------------------------
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Reads `[repo:]namespace[/recipe|/*][@range]`.
|
|
344
|
-
*
|
|
345
|
-
* @param input - The ref as written, for messages.
|
|
346
|
-
* @param trimmed - The ref, trimmed.
|
|
347
|
-
* @param from - Where it was written.
|
|
348
|
-
*/
|
|
349
|
-
function readShort(input: string, trimmed: string, from: RefSource): ParsedRef {
|
|
350
|
-
let body = trimmed;
|
|
351
|
-
let range: string | undefined;
|
|
352
|
-
const atIndex = body.indexOf("@");
|
|
353
|
-
if (atIndex !== -1) {
|
|
354
|
-
if (body.indexOf("@", atIndex + 1) !== -1) {
|
|
355
|
-
throw refError(input, from, "a ref may carry at most one '@' version range.");
|
|
356
|
-
}
|
|
357
|
-
range = body.slice(atIndex + 1).trim();
|
|
358
|
-
body = body.slice(0, atIndex).trim();
|
|
359
|
-
if (range.length === 0) {
|
|
360
|
-
throw refError(input, from, "the '@' is not followed by a version range.");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
let repo: string | undefined;
|
|
365
|
-
const colonIndex = body.indexOf(":");
|
|
366
|
-
if (colonIndex !== -1) {
|
|
367
|
-
if (body.indexOf(":", colonIndex + 1) !== -1) {
|
|
368
|
-
throw refError(input, from, "a ref may carry at most one 'repo:' qualifier.");
|
|
369
|
-
}
|
|
370
|
-
repo = body.slice(0, colonIndex);
|
|
371
|
-
body = body.slice(colonIndex + 1);
|
|
372
|
-
if (repo.length === 0) {
|
|
373
|
-
throw refError(input, from, "the repo qualifier before ':' is empty.");
|
|
374
|
-
}
|
|
375
|
-
if (!NAME_ANY_CASE.test(repo)) {
|
|
376
|
-
throw refError(
|
|
377
|
-
input,
|
|
378
|
-
from,
|
|
379
|
-
`the repo qualifier '${repo}' must be kebab-case: a letter, then letters, digits ` +
|
|
380
|
-
"or hyphens."
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const segments = body.split("/");
|
|
386
|
-
let wildcard = false;
|
|
387
|
-
if (segments.length === 2 && segments[1] === "*") {
|
|
388
|
-
segments.pop();
|
|
389
|
-
wildcard = true;
|
|
390
|
-
}
|
|
391
|
-
if (segments.length > 2) {
|
|
392
|
-
throw refError(
|
|
393
|
-
input,
|
|
394
|
-
from,
|
|
395
|
-
"a short ref has at most two path segments, a namespace and a recipe. A folder path " +
|
|
396
|
-
"inside a repository is written as a URL copied from the browser instead."
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const [namespace, recipe] = segments;
|
|
401
|
-
if (namespace === undefined || namespace.length === 0) {
|
|
402
|
-
throw refError(input, from, "the namespace is empty.");
|
|
403
|
-
}
|
|
404
|
-
checkName(input, from, "namespace", namespace);
|
|
405
|
-
if (recipe !== undefined) {
|
|
406
|
-
if (recipe.length === 0) throw refError(input, from, "the recipe name after '/' is empty.");
|
|
407
|
-
checkName(input, from, "recipe name", recipe);
|
|
408
|
-
}
|
|
409
|
-
if (range !== undefined) checkRange(input, from, range, recipe !== undefined);
|
|
410
|
-
|
|
411
|
-
const parsed: ParsedRef = { namespace };
|
|
412
|
-
if (repo !== undefined) parsed.repo = repo;
|
|
413
|
-
if (recipe !== undefined) parsed.recipe = recipe;
|
|
414
|
-
if (range !== undefined) parsed.range = range;
|
|
415
|
-
|
|
416
|
-
refuseShortForms(input, from, parsed, wildcard);
|
|
417
|
-
return parsed;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Refuses the short forms a place does not allow, saying what to write
|
|
422
|
-
* instead.
|
|
423
|
-
*
|
|
424
|
-
* @param input - The ref as written.
|
|
425
|
-
* @param from - Where it was written.
|
|
426
|
-
* @param parsed - What it reads as.
|
|
427
|
-
* @param wildcard - Whether a namespace was spelled `namespace/*`.
|
|
428
|
-
*/
|
|
429
|
-
function refuseShortForms(
|
|
430
|
-
input: string,
|
|
431
|
-
from: RefSource,
|
|
432
|
-
parsed: ParsedRef,
|
|
433
|
-
wildcard: boolean
|
|
434
|
-
): void {
|
|
435
|
-
const key = refKey(parsed).toLowerCase();
|
|
436
|
-
|
|
437
|
-
if (parsed.repo !== undefined && from !== RefSource.CommandLine) {
|
|
438
|
-
if (from === RefSource.Manifest) {
|
|
439
|
-
throw refused(
|
|
440
|
-
input,
|
|
441
|
-
from,
|
|
442
|
-
"a 'repo:' qualifier names a short name that only the consuming project knows, so " +
|
|
443
|
-
"it cannot appear in a published manifest.",
|
|
444
|
-
`'${key}' for a recipe in this same repository, or name the other repository by ` +
|
|
445
|
-
`its location, as in 'github://owner/repository/${key}'`
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
throw refused(
|
|
449
|
-
input,
|
|
450
|
-
from,
|
|
451
|
-
from === RefSource.Config
|
|
452
|
-
? "the repository a subscription resolves into is recorded in the lockfile, not in " +
|
|
453
|
-
"the key. To choose between repositories, run " +
|
|
454
|
-
`'sous subscribe ${parsed.repo}:${key}', which records the choice.`
|
|
455
|
-
: "a stored key never names a repository.",
|
|
456
|
-
`'${key}'`
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (from === RefSource.Config || from === RefSource.Lockfile) {
|
|
461
|
-
if (parsed.range !== undefined) {
|
|
462
|
-
throw refused(
|
|
463
|
-
input,
|
|
464
|
-
from,
|
|
465
|
-
from === RefSource.Config
|
|
466
|
-
? "a subscription's version range belongs in the entry's own 'range' field."
|
|
467
|
-
: "a stored key carries no version range.",
|
|
468
|
-
from === RefSource.Config ? `'${key}': { range: '${parsed.range}' }` : `'${key}'`
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
if (wildcard) {
|
|
472
|
-
throw refused(input, from, "a whole namespace is stored as its name alone.", `'${key}'`);
|
|
473
|
-
}
|
|
474
|
-
if (!NAME_STORED.test(parsed.namespace) || (parsed.recipe !== undefined && !NAME_STORED.test(parsed.recipe))) {
|
|
475
|
-
throw refused(input, from, "stored names are lowercase.", `'${key}'`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// --- Located refs ---------------------------------------------------------------------------------
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Reads a ref that names a location: a provider-scheme locator, an ordinary
|
|
484
|
-
* URL, an SSH remote or a host path, each optionally followed by `@<range>`.
|
|
485
|
-
*
|
|
486
|
-
* @param input - The ref as written, for messages.
|
|
487
|
-
* @param trimmed - The ref, trimmed.
|
|
488
|
-
* @param from - Where it was written.
|
|
489
|
-
* @param providers - The providers that read locations.
|
|
490
|
-
*/
|
|
491
|
-
function readLocated(
|
|
492
|
-
input: string,
|
|
493
|
-
trimmed: string,
|
|
494
|
-
from: RefSource,
|
|
495
|
-
providers: RepoProvider[]
|
|
496
|
-
): RefReading[] {
|
|
497
|
-
if (from === RefSource.Config) {
|
|
498
|
-
throw refused(
|
|
499
|
-
input,
|
|
500
|
-
from,
|
|
501
|
-
"a config file names a subscription by its short form; the repository it lives in is " +
|
|
502
|
-
"a separate entry under 'repos'.",
|
|
503
|
-
`'namespace/recipe', or run 'sous subscribe ${trimmed}', which adds the repository ` +
|
|
504
|
-
"and writes the subscription in its short form"
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
if (from === RefSource.Lockfile) {
|
|
508
|
-
throw refused(input, from, "a stored key never names a location.", "'namespace/recipe'");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// The range is whatever follows the last '@' after the last '/', so the '@'
|
|
512
|
-
// of an SSH remote ('git@host:...') is never mistaken for one.
|
|
513
|
-
let body = trimmed;
|
|
514
|
-
let range: string | undefined;
|
|
515
|
-
const at = body.lastIndexOf("@");
|
|
516
|
-
if (at !== -1 && at > body.lastIndexOf("/")) {
|
|
517
|
-
range = body.slice(at + 1).trim();
|
|
518
|
-
body = body.slice(0, at).trim();
|
|
519
|
-
if (range.length === 0) {
|
|
520
|
-
throw refError(input, from, "the '@' is not followed by a version range.");
|
|
521
|
-
}
|
|
522
|
-
checkRange(input, from, range, true);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const { provider, host, segments } = splitLocation(input, body, from, providers);
|
|
526
|
-
const locationReadings = provider.readLocation({ host, segments });
|
|
527
|
-
|
|
528
|
-
const readings: RefReading[] = [];
|
|
529
|
-
const dropped: Dropped[] = [];
|
|
530
|
-
|
|
531
|
-
for (const read of locationReadings) {
|
|
532
|
-
const location = locationOf(provider, host, read.repoPath);
|
|
533
|
-
if (location === undefined) continue;
|
|
534
|
-
|
|
535
|
-
if (read.browsed !== undefined) {
|
|
536
|
-
const browsed = read.browsed.replace(/^\/+|\/+$/g, "");
|
|
537
|
-
if (browsed.length === 0) continue;
|
|
538
|
-
readings.push({ location, browsed, ...(range === undefined ? {} : { range }) });
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const named = read.named ?? [];
|
|
543
|
-
if (named.length === 0) {
|
|
544
|
-
if (range === undefined) readings.push({ location, repository: true });
|
|
545
|
-
else dropped.push({ reason: "a version range applies to a recipe, not to a repository." });
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const reading = namedReading(location, named, range);
|
|
550
|
-
if ("reason" in reading) dropped.push(reading);
|
|
551
|
-
else readings.push(reading);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (readings.length === 0) {
|
|
555
|
-
throw refError(
|
|
556
|
-
input,
|
|
557
|
-
from,
|
|
558
|
-
dropped[0]?.reason ??
|
|
559
|
-
`the ${provider.id} provider cannot read a namespace or a recipe from this location. ` +
|
|
560
|
-
`After the repository, a location names a namespace and optionally a recipe, as in ` +
|
|
561
|
-
`'${provider.formatLocator(host, "owner/repository", "workflow/alpha")}'.`
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// A reading of the whole repository is only kept when nothing more specific
|
|
566
|
-
// was read, because a namespace or a recipe is what a ref is for.
|
|
567
|
-
const specific = readings.filter((reading) => !isRepositoryReading(reading));
|
|
568
|
-
if (specific.length > 0) return specific;
|
|
569
|
-
|
|
570
|
-
// A dependency is a namespace or a recipe; a whole repository is not one.
|
|
571
|
-
if (from === RefSource.Manifest) {
|
|
572
|
-
const where = readings[0]!.location!;
|
|
573
|
-
throw refused(
|
|
574
|
-
input,
|
|
575
|
-
from,
|
|
576
|
-
`it names the repository at ${where.url} and nothing inside it, and a dependency is a ` +
|
|
577
|
-
"namespace or a recipe.",
|
|
578
|
-
`'${provider.formatLocator(where.host, where.repoPath, "namespace/recipe")}'`
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
return readings;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Takes the host-agnostic parts out of a location: which provider reads it,
|
|
586
|
-
* the host, and the path segments after the host.
|
|
587
|
-
*
|
|
588
|
-
* @param input - The ref as written, for messages.
|
|
589
|
-
* @param body - The location, without its range.
|
|
590
|
-
* @param from - Where it was written.
|
|
591
|
-
* @param providers - The providers to choose from.
|
|
592
|
-
*/
|
|
593
|
-
function splitLocation(
|
|
594
|
-
input: string,
|
|
595
|
-
body: string,
|
|
596
|
-
from: RefSource,
|
|
597
|
-
providers: RepoProvider[]
|
|
598
|
-
): { provider: RepoProvider; host: string; segments: string[] } {
|
|
599
|
-
const known = providers
|
|
600
|
-
.filter((provider) => provider.defaultHost !== undefined)
|
|
601
|
-
.map((provider) => provider.id)
|
|
602
|
-
.sort()
|
|
603
|
-
.join(", ");
|
|
604
|
-
|
|
605
|
-
const scheme = SCHEME_PATTERN.exec(body);
|
|
606
|
-
if (scheme !== null) {
|
|
607
|
-
const name = scheme[1]!.toLowerCase();
|
|
608
|
-
|
|
609
|
-
if (LOCAL_SCHEMES.has(name)) {
|
|
610
|
-
throw refused(
|
|
611
|
-
input,
|
|
612
|
-
from,
|
|
613
|
-
from === RefSource.Manifest
|
|
614
|
-
? "a local repository is a consumer's convenience, not a published location, so a " +
|
|
615
|
-
"manifest cannot depend on one."
|
|
616
|
-
: "a repository on this machine is added by its path, and its recipes are then " +
|
|
617
|
-
"named by their short form.",
|
|
618
|
-
from === RefSource.Manifest
|
|
619
|
-
? "the recipe's published location, such as 'github://owner/repository/namespace/recipe'"
|
|
620
|
-
: "'sous repo add <path>', then 'namespace/recipe'"
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (URL_SCHEMES.has(name)) {
|
|
625
|
-
let url: URL;
|
|
626
|
-
try {
|
|
627
|
-
url = new URL(body);
|
|
628
|
-
} catch {
|
|
629
|
-
throw refError(input, from, "the URL could not be read.");
|
|
630
|
-
}
|
|
631
|
-
const host = url.host.toLowerCase();
|
|
632
|
-
const segments = pathSegments(url.pathname);
|
|
633
|
-
return { provider: providerForHost(input, from, host, segments, providers), host, segments };
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const provider = providerById(name, providers);
|
|
637
|
-
if (provider === undefined || provider.defaultHost === undefined) {
|
|
638
|
-
throw refError(
|
|
639
|
-
input,
|
|
640
|
-
from,
|
|
641
|
-
`'${name}' is not a provider sous can read a location from. The scheme of a locator ` +
|
|
642
|
-
`is the provider's own identifier; sous ships these: ${known}. An ordinary ` +
|
|
643
|
-
"'https://' URL works too."
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
let segments = pathSegments(scheme[2]!);
|
|
647
|
-
let host = provider.defaultHost;
|
|
648
|
-
if (segments[0]?.includes(".") === true) {
|
|
649
|
-
host = segments[0].toLowerCase();
|
|
650
|
-
segments = segments.slice(1);
|
|
651
|
-
}
|
|
652
|
-
return { provider, host, segments };
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const scp = SCP_PATTERN.exec(body);
|
|
656
|
-
if (scp !== null) {
|
|
657
|
-
const host = scp[1]!.toLowerCase();
|
|
658
|
-
const segments = pathSegments(scp[2]!);
|
|
659
|
-
return { provider: providerForHost(input, from, host, segments, providers), host, segments };
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const all = pathSegments(body);
|
|
663
|
-
const host = all[0]!.toLowerCase();
|
|
664
|
-
const segments = all.slice(1);
|
|
665
|
-
return { provider: providerForHost(input, from, host, segments, providers), host, segments };
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* The provider that recognizes a host, asked with the repository URL the
|
|
670
|
-
* location would start with.
|
|
671
|
-
*/
|
|
672
|
-
function providerForHost(
|
|
673
|
-
input: string,
|
|
674
|
-
from: RefSource,
|
|
675
|
-
host: string,
|
|
676
|
-
segments: string[],
|
|
677
|
-
providers: RepoProvider[]
|
|
678
|
-
): RepoProvider {
|
|
679
|
-
const probe = `https://${host}/${segments.slice(0, 2).join("/")}`;
|
|
680
|
-
const provider = detectProvider(probe, providers);
|
|
681
|
-
if (provider !== undefined && provider.defaultHost !== undefined) return provider;
|
|
682
|
-
const ids = providers
|
|
683
|
-
.filter((entry) => entry.defaultHost !== undefined)
|
|
684
|
-
.map((entry) => entry.id)
|
|
685
|
-
.sort();
|
|
686
|
-
throw refError(
|
|
687
|
-
input,
|
|
688
|
-
from,
|
|
689
|
-
`sous does not recognize the host '${host}'. For a self-hosted instance, write the ` +
|
|
690
|
-
`location with the provider's scheme and the host, as in ` +
|
|
691
|
-
`'${ids[0] ?? "gitlab"}://${host}/owner/repository/namespace/recipe' (sous ships these ` +
|
|
692
|
-
`providers: ${ids.join(", ")}).`
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/** A URL path's segments, decoded, with empty ones dropped. */
|
|
697
|
-
function pathSegments(pathname: string): string[] {
|
|
698
|
-
return pathname
|
|
699
|
-
.split(/[?#]/)[0]!
|
|
700
|
-
.split("/")
|
|
701
|
-
.map((segment) => {
|
|
702
|
-
try {
|
|
703
|
-
return decodeURIComponent(segment);
|
|
704
|
-
} catch {
|
|
705
|
-
return segment;
|
|
706
|
-
}
|
|
707
|
-
})
|
|
708
|
-
.filter((segment) => segment.length > 0);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Where one reading's repository lives, or undefined when the provider cannot
|
|
713
|
-
* take its path apart (a path of one segment, say).
|
|
714
|
-
*/
|
|
715
|
-
function locationOf(
|
|
716
|
-
provider: RepoProvider,
|
|
717
|
-
host: string,
|
|
718
|
-
repoPath: string
|
|
719
|
-
): RefLocation | undefined {
|
|
720
|
-
const url = `https://${host}/${repoPath}`;
|
|
721
|
-
try {
|
|
722
|
-
return {
|
|
723
|
-
provider: provider.id,
|
|
724
|
-
host,
|
|
725
|
-
repoPath,
|
|
726
|
-
identity: repoIdentity(provider.canonicalize(url)),
|
|
727
|
-
url,
|
|
728
|
-
};
|
|
729
|
-
} catch {
|
|
730
|
-
return undefined;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* The reading named by the segments after a repository, or the reason it is
|
|
736
|
-
* not one.
|
|
737
|
-
*/
|
|
738
|
-
function namedReading(
|
|
739
|
-
location: RefLocation,
|
|
740
|
-
named: string[],
|
|
741
|
-
range: string | undefined
|
|
742
|
-
): ParsedRef | Dropped {
|
|
743
|
-
const segments = named.length === 2 && named[1] === "*" ? [named[0]!] : named;
|
|
744
|
-
const where = location.url;
|
|
745
|
-
|
|
746
|
-
if (segments.length > 2) {
|
|
747
|
-
return {
|
|
748
|
-
reason:
|
|
749
|
-
`after the repository ${where}, '${segments.join("/")}' is ` +
|
|
750
|
-
`${segments.length} segments, and a location names a namespace and optionally a ` +
|
|
751
|
-
"recipe. A folder path is written as the URL copied from the browser, with its " +
|
|
752
|
-
"'tree/<branch>/' part.",
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const [namespace, recipe] = segments as [string, string | undefined];
|
|
757
|
-
for (const [what, name] of [
|
|
758
|
-
["namespace", namespace],
|
|
759
|
-
["recipe name", recipe],
|
|
760
|
-
] as const) {
|
|
761
|
-
if (name !== undefined && !NAME_ANY_CASE.test(name)) {
|
|
762
|
-
return {
|
|
763
|
-
reason:
|
|
764
|
-
`the ${what} '${name}' must be kebab-case: a letter, then letters, digits or ` +
|
|
765
|
-
"hyphens.",
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (range !== undefined && recipe === undefined) {
|
|
771
|
-
return {
|
|
772
|
-
reason:
|
|
773
|
-
"a version range applies to a recipe, and namespaces are not versioned. Name a " +
|
|
774
|
-
"recipe, as in 'github://owner/repository/workflow/task-files@^1.2.0'.",
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
return {
|
|
779
|
-
location,
|
|
780
|
-
namespace,
|
|
781
|
-
...(recipe === undefined ? {} : { recipe }),
|
|
782
|
-
...(range === undefined ? {} : { range }),
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// --- Shared pieces --------------------------------------------------------------------------------
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* A reading with its names in the case a place stores. A manifest is published
|
|
790
|
-
* and every published name is lowercase, so a manifest's names are lowercased;
|
|
791
|
-
* the command line keeps what was typed, and matching settles it (exact first,
|
|
792
|
-
* then ignoring case).
|
|
793
|
-
*/
|
|
794
|
-
function storedCase(reading: RefReading, from: RefSource): RefReading {
|
|
795
|
-
if (from !== RefSource.Manifest || !isNamedReading(reading)) return reading;
|
|
796
|
-
return {
|
|
797
|
-
...reading,
|
|
798
|
-
namespace: reading.namespace.toLowerCase(),
|
|
799
|
-
...(reading.recipe === undefined ? {} : { recipe: reading.recipe.toLowerCase() }),
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/** Checks one name, which may be written in any case. */
|
|
804
|
-
function checkName(input: string, from: RefSource, what: string, name: string): void {
|
|
805
|
-
if (NAME_ANY_CASE.test(name)) return;
|
|
806
|
-
throw refError(
|
|
807
|
-
input,
|
|
808
|
-
from,
|
|
809
|
-
`the ${what} '${name}' must be kebab-case: a letter, then letters, digits or hyphens.`
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/** Checks a version range, and that there is a recipe for it to apply to. */
|
|
814
|
-
function checkRange(input: string, from: RefSource, range: string, hasRecipe: boolean): void {
|
|
815
|
-
if (!hasRecipe) {
|
|
816
|
-
throw refError(
|
|
817
|
-
input,
|
|
818
|
-
from,
|
|
819
|
-
"a version range applies to a recipe, and namespaces are not versioned. Name a " +
|
|
820
|
-
"recipe, as in 'workflow/task-files@^1.2.0'."
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
if (semver.validRange(range) === null) {
|
|
824
|
-
throw refError(
|
|
825
|
-
input,
|
|
826
|
-
from,
|
|
827
|
-
`'${range}' is not a version range. Ranges follow npm's rules, such as '^1.2.0', ` +
|
|
828
|
-
"'~2.1', '>=1.0.0 <2.0.0' or '*'."
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/** A ConfigError for a ref that fits no form, quoting it and showing the forms allowed there. */
|
|
834
|
-
function refError(input: string, from: RefSource, problem: string): ConfigError {
|
|
835
|
-
return new ConfigError(`Invalid ref '${input}': ${problem}\n ${SYNTAX_HELP[from]}`);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/** A ConfigError for a form this place does not allow, saying what to write instead. */
|
|
839
|
-
function refused(input: string, from: RefSource, reason: string, instead: string): ConfigError {
|
|
840
|
-
return new ConfigError(
|
|
841
|
-
`The ref '${input.trim()}' cannot be written ${PLACE_LABELS[from]}: ${reason}\n` +
|
|
842
|
-
` Write ${instead} instead.`
|
|
843
|
-
);
|
|
844
|
-
}
|