@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Printing a ref: its key (what everything is stored and compared under) and
|
|
3
|
+
* its canonical written form (how sous prints it).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
builtInProviders,
|
|
8
|
+
providerById,
|
|
9
|
+
type RepoProvider,
|
|
10
|
+
} from "../../lib/repos/providers/index.js";
|
|
11
|
+
import { repoOf } from "./parts.js";
|
|
12
|
+
import { KIND_LABELS, type RepoRef, type SousRef } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The part of a key that names the repository: its short name, or its
|
|
16
|
+
* identity when it was named by location.
|
|
17
|
+
*/
|
|
18
|
+
function repoPart(repo: RepoRef | undefined): string | undefined {
|
|
19
|
+
if (repo === undefined) return undefined;
|
|
20
|
+
return repo.name ?? repo.location?.identity;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The path part of a key, without any repository.
|
|
25
|
+
*
|
|
26
|
+
* @param ref - Any ref.
|
|
27
|
+
*/
|
|
28
|
+
function pathPart(ref: SousRef): string {
|
|
29
|
+
switch (ref.kind) {
|
|
30
|
+
case "repo":
|
|
31
|
+
return ref.browsed === undefined ? "" : `tree/${ref.browsed}`;
|
|
32
|
+
case "namespace":
|
|
33
|
+
return ref.name;
|
|
34
|
+
case "recipe":
|
|
35
|
+
return ref.namespace === undefined ? ref.name : `${ref.namespace.name}/${ref.name}`;
|
|
36
|
+
case "recipeFile":
|
|
37
|
+
return `${pathPart(ref.recipe)}/${ref.path}`;
|
|
38
|
+
case "variable": {
|
|
39
|
+
const recipe = ref.recipe === undefined ? "" : `${pathPart(ref.recipe)}.`;
|
|
40
|
+
return `${recipe}${ref.name}`;
|
|
41
|
+
}
|
|
42
|
+
case "envVar":
|
|
43
|
+
return ref.name;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The canonical key of a ref, with its version range and query values left
|
|
49
|
+
* off, so the same thing is never keyed twice under two spellings. A
|
|
50
|
+
* repository qualifier is part of the key, because the same recipe name in two
|
|
51
|
+
* repositories is two things.
|
|
52
|
+
*
|
|
53
|
+
* refKey(namespace("workflow")); // -> "workflow"
|
|
54
|
+
* refKey(recipe("task-files", "workflow", "sous-recipes")); // -> "sous-recipes:workflow/task-files"
|
|
55
|
+
* refKey(variable("apiUrl", recipe)); // -> "workflow/task-files.apiUrl"
|
|
56
|
+
*
|
|
57
|
+
* @param ref - Any ref.
|
|
58
|
+
*/
|
|
59
|
+
export function refKey(ref: SousRef): string {
|
|
60
|
+
const repo = ref.kind === "envVar" ? undefined : repoPart(repoOf(ref));
|
|
61
|
+
const rest = pathPart(ref);
|
|
62
|
+
if (ref.kind === "repo") return repo === undefined ? rest : rest === "" ? repo : `${repo}:${rest}`;
|
|
63
|
+
return repo === undefined ? rest : `${repo}:${rest}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The key of a ref together with its kind, which is what tells a namespace
|
|
68
|
+
* `workflow` from a repository `workflow` apart.
|
|
69
|
+
*
|
|
70
|
+
* @param ref - Any ref.
|
|
71
|
+
*/
|
|
72
|
+
export function refIdentity(ref: SousRef): string {
|
|
73
|
+
return `${ref.kind} ${refKey(ref)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes the repository qualifier in front of what is named inside it: `name:`
|
|
78
|
+
* for a repository this project knows by short name (whatever else it knows of
|
|
79
|
+
* it), and the provider's canonical locator for one known only by location.
|
|
80
|
+
*/
|
|
81
|
+
function qualified(
|
|
82
|
+
repo: RepoRef | undefined,
|
|
83
|
+
rest: string,
|
|
84
|
+
providers: RepoProvider[]
|
|
85
|
+
): string {
|
|
86
|
+
if (repo === undefined) return rest;
|
|
87
|
+
if (repo.name !== undefined) return rest === "" ? repo.name : `${repo.name}:${rest}`;
|
|
88
|
+
if (repo.location !== undefined) {
|
|
89
|
+
const { host, repoPath } = repo.location;
|
|
90
|
+
const provider = providerById(repo.location.provider, providers);
|
|
91
|
+
if (provider !== undefined && provider.id !== "local") {
|
|
92
|
+
return provider.formatLocator(host, repoPath, rest);
|
|
93
|
+
}
|
|
94
|
+
return rest === "" ? repo.location.url : `${repo.location.url}/${rest}`;
|
|
95
|
+
}
|
|
96
|
+
return rest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The canonical written form of a ref, which is how sous prints it: the short
|
|
101
|
+
* form for a short ref and each provider's own locator for a located one, then
|
|
102
|
+
* the version range, then the query values.
|
|
103
|
+
*
|
|
104
|
+
* formatRef(parse("https://github.com/o/r/w/a@^1")[0]); // -> "github://o/r/w/a@^1"
|
|
105
|
+
*
|
|
106
|
+
* @param ref - Any ref.
|
|
107
|
+
* @param providers - The providers that format locators. Defaults to the built-ins.
|
|
108
|
+
*/
|
|
109
|
+
export function formatRef(ref: SousRef, providers: RepoProvider[] = builtInProviders()): string {
|
|
110
|
+
const body = qualified(ref.kind === "envVar" ? undefined : repoOf(ref), pathPart(ref), providers);
|
|
111
|
+
const written =
|
|
112
|
+
ref.kind === "recipe" || ref.kind === "recipeFile" || ref.kind === "repo"
|
|
113
|
+
? ref.range
|
|
114
|
+
: undefined;
|
|
115
|
+
const range = written === undefined ? "" : `@${written}`;
|
|
116
|
+
const pairs = Object.entries(ref.vars ?? {}).map(
|
|
117
|
+
([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
|
|
118
|
+
);
|
|
119
|
+
return `${body}${range}${pairs.length === 0 ? "" : `?${pairs.join("&")}`}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What kind of thing a ref names, in a sentence.
|
|
124
|
+
*
|
|
125
|
+
* @param ref - Any ref.
|
|
126
|
+
*/
|
|
127
|
+
export function refKindLabel(ref: SousRef): string {
|
|
128
|
+
return KIND_LABELS[ref.kind];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How a ref is written to a person, so they can type it back: its key when a
|
|
133
|
+
* repository's short name qualifies it, and its canonical locator when only a
|
|
134
|
+
* location does (a ref that has not been matched to a repository this project
|
|
135
|
+
* knows by name).
|
|
136
|
+
*
|
|
137
|
+
* refSpelling(recipe "task-files" in "sous-recipes") // -> "sous-recipes:workflow/task-files"
|
|
138
|
+
* refSpelling(a recipe at a located repository) // -> its canonical locator
|
|
139
|
+
*
|
|
140
|
+
* @param ref - Any ref.
|
|
141
|
+
*/
|
|
142
|
+
export function refSpelling(ref: SousRef): string {
|
|
143
|
+
const repo = ref.kind === "envVar" ? undefined : repoOf(ref);
|
|
144
|
+
if (repo !== undefined && repo.name === undefined && repo.location !== undefined) {
|
|
145
|
+
return formatRef(ref);
|
|
146
|
+
}
|
|
147
|
+
return refKey(ref);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The repository a person is told a ref lives in: its short name, or where it lives. */
|
|
151
|
+
function repoLabel(repo: RepoRef | undefined): string {
|
|
152
|
+
return repo?.name ?? repo?.location?.url ?? "unknown";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Describes one ref in the words a person choosing between several needs: the
|
|
157
|
+
* fully qualified name, and what it actually means.
|
|
158
|
+
*
|
|
159
|
+
* @param ref - A known ref, as a lookup returns it.
|
|
160
|
+
*/
|
|
161
|
+
export function describeRef(ref: SousRef): string {
|
|
162
|
+
const summary = ref.description === undefined ? "" : `: ${ref.description}`;
|
|
163
|
+
const key = refSpelling(ref);
|
|
164
|
+
|
|
165
|
+
switch (ref.kind) {
|
|
166
|
+
case "repo": {
|
|
167
|
+
const where = ref.location?.url;
|
|
168
|
+
return `${key} (the repository '${ref.name ?? key}'${where === undefined ? "" : `, at ${where}`})`;
|
|
169
|
+
}
|
|
170
|
+
case "namespace":
|
|
171
|
+
return (
|
|
172
|
+
`${key} (the whole namespace '${ref.name}' in the ` +
|
|
173
|
+
`repository '${repoLabel(ref.repo)}')`
|
|
174
|
+
);
|
|
175
|
+
case "recipe":
|
|
176
|
+
return (
|
|
177
|
+
`${key} (the recipe '${ref.name}' in the namespace ` +
|
|
178
|
+
`'${ref.namespace?.name ?? "unknown"}' of the repository ` +
|
|
179
|
+
`'${repoLabel(repoOf(ref))}'${summary})`
|
|
180
|
+
);
|
|
181
|
+
case "recipeFile":
|
|
182
|
+
return `${key} (the file '${ref.path}' of the recipe '${pathPart(ref.recipe)}'${summary})`;
|
|
183
|
+
case "variable":
|
|
184
|
+
return (
|
|
185
|
+
`${key} (the variable '${ref.name}' of the recipe ` +
|
|
186
|
+
`'${ref.recipe === undefined ? "unknown" : pathPart(ref.recipe)}'${summary})`
|
|
187
|
+
);
|
|
188
|
+
case "envVar": {
|
|
189
|
+
const first = ref.variables?.[0];
|
|
190
|
+
const answers =
|
|
191
|
+
first === undefined
|
|
192
|
+
? ""
|
|
193
|
+
: `answering '${first.name}' of the recipe '${first.recipe === undefined ? "unknown" : pathPart(first.recipe)}'`;
|
|
194
|
+
return `${ref.name} (the environment variable${answers === "" ? "" : ` ${answers}`}${summary})`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names, glob patterns and how a written name is compared with a known one.
|
|
3
|
+
*
|
|
4
|
+
* Every name or path part of a ref may be a glob pattern (`*`, `**`, `?`,
|
|
5
|
+
* `[..]`, `{a,b}`), matched with minimatch. Plain names are compared as
|
|
6
|
+
* strings. Either way the exact spelling is tried first and ignoring case is
|
|
7
|
+
* the fallback, so the caller can tell which one matched.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { minimatch } from "minimatch";
|
|
11
|
+
|
|
12
|
+
/** A name as the parser accepts it: kebab-case, in any case. */
|
|
13
|
+
export const NAME_ANY_CASE = /^[a-z][a-z0-9-]*$/i;
|
|
14
|
+
|
|
15
|
+
/** A name as it is stored: lowercase kebab-case. */
|
|
16
|
+
export const NAME_STORED = /^[a-z][a-z0-9-]*$/;
|
|
17
|
+
|
|
18
|
+
/** A variable name, as a recipe's author writes it: camelCase. */
|
|
19
|
+
export const VARIABLE_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
20
|
+
|
|
21
|
+
/** An environment variable name: whatever a shell accepts. */
|
|
22
|
+
export const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23
|
+
|
|
24
|
+
/** The characters a glob pattern is made of. */
|
|
25
|
+
const GLOB_CHARS = /[*?[\]{}]/;
|
|
26
|
+
|
|
27
|
+
/** The characters a glob pattern standing for a name may use. */
|
|
28
|
+
const GLOB_NAME = /^[A-Za-z0-9_\-.*?[\]{},!^]+$/;
|
|
29
|
+
|
|
30
|
+
/** True when a string holds glob syntax. */
|
|
31
|
+
export function hasGlob(text: string): boolean {
|
|
32
|
+
return GLOB_CHARS.test(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when a string is a glob pattern that could stand for a name. */
|
|
36
|
+
export function isGlobName(text: string): boolean {
|
|
37
|
+
return hasGlob(text) && GLOB_NAME.test(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Splits text at `/`, except inside braces, so `{a/b,c}` stays whole.
|
|
42
|
+
*
|
|
43
|
+
* splitSegments("a/{b/c,d}/e"); // -> ["a", "{b/c,d}", "e"]
|
|
44
|
+
*
|
|
45
|
+
* @param text - The text to split.
|
|
46
|
+
*/
|
|
47
|
+
export function splitSegments(text: string): string[] {
|
|
48
|
+
const segments: string[] = [];
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let current = "";
|
|
51
|
+
for (const character of text) {
|
|
52
|
+
if (character === "{") depth += 1;
|
|
53
|
+
if (character === "}" && depth > 0) depth -= 1;
|
|
54
|
+
if (character === "/" && depth === 0) {
|
|
55
|
+
segments.push(current);
|
|
56
|
+
current = "";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
current += character;
|
|
60
|
+
}
|
|
61
|
+
segments.push(current);
|
|
62
|
+
return segments;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How a written name matched a known one. */
|
|
66
|
+
export type NameMatch = "exact" | "folded" | undefined;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a written name (or glob pattern) matches a known one: exactly, only
|
|
70
|
+
* ignoring case, or not at all.
|
|
71
|
+
*
|
|
72
|
+
* matchName("Workflow", "workflow"); // -> "folded"
|
|
73
|
+
* matchName("work*", "workflow"); // -> "exact"
|
|
74
|
+
*
|
|
75
|
+
* @param written - The name or pattern as written.
|
|
76
|
+
* @param known - The name as it is spelled where it is published.
|
|
77
|
+
*/
|
|
78
|
+
export function matchName(written: string, known: string): NameMatch {
|
|
79
|
+
if (hasGlob(written)) {
|
|
80
|
+
if (minimatch(known, written, { dot: true })) return "exact";
|
|
81
|
+
return minimatch(known, written, { dot: true, nocase: true }) ? "folded" : undefined;
|
|
82
|
+
}
|
|
83
|
+
if (written === known) return "exact";
|
|
84
|
+
return written.toLowerCase() === known.toLowerCase() ? "folded" : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The names that match a written one: those spelled exactly as written, or,
|
|
89
|
+
* when there is none, those that differ only in case.
|
|
90
|
+
*
|
|
91
|
+
* matchNames("Workflow", ["workflow", "tooling"]); // -> ["workflow"]
|
|
92
|
+
* matchNames("workflow", ["workflow", "Workflow"]); // -> ["workflow"]
|
|
93
|
+
*
|
|
94
|
+
* @param written - The name as written.
|
|
95
|
+
* @param names - The names it could mean.
|
|
96
|
+
*/
|
|
97
|
+
export function matchNames(written: string, names: Iterable<string>): string[] {
|
|
98
|
+
const all = [...names];
|
|
99
|
+
const exact = all.filter((name) => matchName(written, name) === "exact");
|
|
100
|
+
if (exact.length > 0) return exact;
|
|
101
|
+
return all.filter((name) => matchName(written, name) === "folded");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Orders two strings by their UTF-8 bytes, so the order is the same on every
|
|
106
|
+
* machine whatever its locale.
|
|
107
|
+
*
|
|
108
|
+
* ["b", "B", "a"].sort(compareBytewise); // -> ["B", "a", "b"]
|
|
109
|
+
*
|
|
110
|
+
* @param left - The first string.
|
|
111
|
+
* @param right - The second string.
|
|
112
|
+
*/
|
|
113
|
+
export function compareBytewise(left: string, right: string): number {
|
|
114
|
+
return Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
115
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `#` names: the built-in names an include line, a `{% render %}` path or
|
|
3
|
+
* an `entryGlob` may start with, such as `#project`.
|
|
4
|
+
*
|
|
5
|
+
* `~` keeps meaning the home directory and recipe namespaces; `#` is for names
|
|
6
|
+
* sous or a plugin provides. Each name is registered once, with the directories
|
|
7
|
+
* it stands for, and a registration that repeats a name is an error naming both
|
|
8
|
+
* registrants. A later layer registers a view the same way.
|
|
9
|
+
*
|
|
10
|
+
* A plugin adds a name by binding one more `HashName` under
|
|
11
|
+
* `REF_TOKENS.HashName` on a container it builds with `createRefContainer()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ConfigError } from "../../lib/errors.js";
|
|
15
|
+
import { compareBytewise } from "./glob.js";
|
|
16
|
+
import { makeInjectable } from "./injectable.js";
|
|
17
|
+
import { REF_TOKENS } from "./tokens.js";
|
|
18
|
+
|
|
19
|
+
/** One file a view lists: where it sits in the view, and the real file behind it. */
|
|
20
|
+
export interface ViewFile {
|
|
21
|
+
/** The path inside the view, without the `#name/` prefix, with `/` separators. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** The real file's absolute path. */
|
|
24
|
+
readonly file: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a view needs to know about the project to list its files. */
|
|
28
|
+
export interface HashViewContext {
|
|
29
|
+
/** The project's `.sous/` directory. */
|
|
30
|
+
sousDir: string;
|
|
31
|
+
/** The environment to read; decides where the recipe store is. */
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** The merged project config, read for the `recipes` key. */
|
|
34
|
+
settings: {
|
|
35
|
+
recipes?: { memories?: { first?: string[] | undefined; exclude?: string[] | undefined } };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One `#` name and what it stands for. */
|
|
40
|
+
export interface HashName {
|
|
41
|
+
/** The name without its `#`: lowercase kebab-case, such as `project`. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** Who registers it, for the error a duplicate raises (`sous`, or a plugin's name). */
|
|
44
|
+
readonly registeredBy: string;
|
|
45
|
+
/** What the name is, in one sentence. */
|
|
46
|
+
readonly description: string;
|
|
47
|
+
/**
|
|
48
|
+
* The directories the name stands for, in the order they are tried. A path
|
|
49
|
+
* after the name is looked up under each in turn, the first that exists
|
|
50
|
+
* wins. Empty when the name has nothing to stand for in this project.
|
|
51
|
+
*
|
|
52
|
+
* @param scope - The settings scope the project's paths come from.
|
|
53
|
+
*/
|
|
54
|
+
bases(scope: Record<string, string>): string[];
|
|
55
|
+
/**
|
|
56
|
+
* A view lists virtual files instead of standing for directories: an include
|
|
57
|
+
* of `#name/<glob>` selects among the listed paths and includes the real
|
|
58
|
+
* files behind them, in the listed order. A view has no bases.
|
|
59
|
+
*
|
|
60
|
+
* @param context - The project the files are listed for.
|
|
61
|
+
*/
|
|
62
|
+
view?(context: HashViewContext): ViewFile[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The characters a registered name may hold. */
|
|
66
|
+
const VALID_NAME = /^[a-z][a-z0-9-]*$/;
|
|
67
|
+
|
|
68
|
+
/** The built-in `#project`: the consuming project's root. */
|
|
69
|
+
export class ProjectHashName implements HashName {
|
|
70
|
+
readonly name = "project";
|
|
71
|
+
readonly registeredBy = "sous";
|
|
72
|
+
readonly description = "the root directory of the project being built.";
|
|
73
|
+
|
|
74
|
+
bases(scope: Record<string, string>): string[] {
|
|
75
|
+
return scope.projectRoot ? [scope.projectRoot] : [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
makeInjectable(ProjectHashName);
|
|
80
|
+
|
|
81
|
+
/** Every registered `#` name. */
|
|
82
|
+
export class HashNameRegistry {
|
|
83
|
+
private readonly entries = new Map<string, HashName>();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param names - The names to register, each once.
|
|
87
|
+
* @throws A ConfigError when a name is invalid or registered twice.
|
|
88
|
+
*/
|
|
89
|
+
constructor(names: HashName[] = []) {
|
|
90
|
+
for (const entry of names) this.register(entry);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Adds a name.
|
|
95
|
+
*
|
|
96
|
+
* @param entry - The name to add.
|
|
97
|
+
* @throws A ConfigError naming both registrants when the name is already taken.
|
|
98
|
+
*/
|
|
99
|
+
register(entry: HashName): void {
|
|
100
|
+
if (!VALID_NAME.test(entry.name)) {
|
|
101
|
+
throw new ConfigError(
|
|
102
|
+
`The name '#${entry.name}' registered by '${entry.registeredBy}' is invalid: a name is ` +
|
|
103
|
+
"lowercase kebab-case, a letter then letters, digits or hyphens."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const existing = this.entries.get(entry.name);
|
|
107
|
+
if (existing !== undefined) {
|
|
108
|
+
throw new ConfigError(
|
|
109
|
+
`The name '#${entry.name}' is registered twice: by '${existing.registeredBy}' and by ` +
|
|
110
|
+
`'${entry.registeredBy}'.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
this.entries.set(entry.name, entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The registered name, or undefined. */
|
|
117
|
+
get(name: string): HashName | undefined {
|
|
118
|
+
return this.entries.get(name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Every registered name, sorted. */
|
|
122
|
+
list(): HashName[] {
|
|
123
|
+
return [...this.entries.values()].sort((left, right) => compareBytewise(left.name, right.name));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Every view's listed files, keyed by the name with its `#`.
|
|
128
|
+
*
|
|
129
|
+
* @param context - The project the files are listed for.
|
|
130
|
+
*/
|
|
131
|
+
viewMap(context: HashViewContext): Record<string, ViewFile[]> {
|
|
132
|
+
const map: Record<string, ViewFile[]> = {};
|
|
133
|
+
for (const entry of this.list()) {
|
|
134
|
+
if (entry.view !== undefined) map[`#${entry.name}`] = entry.view(context);
|
|
135
|
+
}
|
|
136
|
+
return map;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The alias map the registered names make for a project: `#name` to its
|
|
141
|
+
* directories, leaving out a name with none.
|
|
142
|
+
*
|
|
143
|
+
* @param scope - The settings scope the project's paths come from.
|
|
144
|
+
*/
|
|
145
|
+
aliasMap(scope: Record<string, string>): Record<string, string[]> {
|
|
146
|
+
const map: Record<string, string[]> = {};
|
|
147
|
+
for (const entry of this.list()) {
|
|
148
|
+
const bases = entry.bases(scope);
|
|
149
|
+
if (bases.length > 0) map[`#${entry.name}`] = bases;
|
|
150
|
+
}
|
|
151
|
+
return map;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
makeInjectable(HashNameRegistry, [{ multi: REF_TOKENS.HashName }]);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ref resolver service: how every sous command and template turns a
|
|
3
|
+
* written ref into the thing it names.
|
|
4
|
+
*
|
|
5
|
+
* Import from here. `types.ts` says what a ref is, the parser reads every way a
|
|
6
|
+
* text can be one, a pruner per place says which readings count there, a
|
|
7
|
+
* lookup says which exist, `RefResolverService` runs the three in order, and
|
|
8
|
+
* `RefPickerService` settles which meaning a run proceeds with.
|
|
9
|
+
*
|
|
10
|
+
* `EnvVarLookup` is not exported here. It reads the variable ladder, which
|
|
11
|
+
* reaches config discovery, and config discovery reaches this module through
|
|
12
|
+
* the recipe formats, so exporting it from the barrel would make an import
|
|
13
|
+
* cycle that fails at load time. Import it from `lookups/env-var-lookup.js`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export * from "./source.js";
|
|
17
|
+
export * from "./types.js";
|
|
18
|
+
export * from "./parts.js";
|
|
19
|
+
export * from "./glob.js";
|
|
20
|
+
export * from "./location.js";
|
|
21
|
+
export * from "./format.js";
|
|
22
|
+
export * from "./tokens.js";
|
|
23
|
+
export * from "./hash-names.js";
|
|
24
|
+
export * from "./injectable.js";
|
|
25
|
+
export * from "./parser/partial-ref.js";
|
|
26
|
+
export * from "./parser/ref-parser.js";
|
|
27
|
+
export * from "./parser/query-splitter.js";
|
|
28
|
+
export * from "./parser/range-splitter.js";
|
|
29
|
+
export * from "./parser/location-splitter.js";
|
|
30
|
+
export * from "./parser/repo-qualifier-splitter.js";
|
|
31
|
+
export * from "./parser/name-path-splitter.js";
|
|
32
|
+
export * from "./parser/env-var-splitter.js";
|
|
33
|
+
export * from "./pruners/ref-pruner.js";
|
|
34
|
+
export * from "./pruners/rule-helpers.js";
|
|
35
|
+
export * from "./pruners/command-line-pruner.js";
|
|
36
|
+
export * from "./pruners/stored-key-pruner.js";
|
|
37
|
+
export * from "./pruners/manifest-pruner.js";
|
|
38
|
+
export * from "./pruners/include-pruner.js";
|
|
39
|
+
export * from "./lookups/ref-lookup.js";
|
|
40
|
+
export * from "./lookups/chained-lookup.js";
|
|
41
|
+
export * from "./lookups/catalog-matcher.js";
|
|
42
|
+
export * from "./lookups/catalog-lookup.js";
|
|
43
|
+
export * from "./lookups/cached-index-lookup.js";
|
|
44
|
+
export * from "./lookups/recorded-dependency-lookup.js";
|
|
45
|
+
export * from "./lookups/fetched-index-lookup.js";
|
|
46
|
+
export * from "./lookups/variable-lookup.js";
|
|
47
|
+
export * from "./ref-resolve-arguments.js";
|
|
48
|
+
export * from "./ref-resolve-result.js";
|
|
49
|
+
export * from "./ref-resolver-service.js";
|
|
50
|
+
export * from "./ref-picker-service.js";
|
|
51
|
+
export * from "./ref-report.js";
|
|
52
|
+
export * from "./named-refs.js";
|
|
53
|
+
export * from "./dependency.js";
|
|
54
|
+
export * from "./ref-helpers.js";
|
|
55
|
+
export * from "./container.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marking a class as injectable without decorator syntax.
|
|
3
|
+
*
|
|
4
|
+
* sous runs TypeScript through tsx, and the published `bin/run.js` registers
|
|
5
|
+
* tsx from the directory a command runs in, so the `experimentalDecorators`
|
|
6
|
+
* flag in this repository's tsconfig does not reach it; esbuild also emits no
|
|
7
|
+
* decorator metadata. Inversify's decorators are plain functions, though, so
|
|
8
|
+
* calling them here marks every constructor parameter with its token
|
|
9
|
+
* explicitly and needs neither a compiler flag nor `reflect-metadata`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { decorate, inject, injectable, multiInject } from "inversify";
|
|
13
|
+
|
|
14
|
+
/** One constructor parameter: a token bound once, or a token bound many times. */
|
|
15
|
+
export type ConstructorParameter = symbol | { multi: symbol };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Marks a class injectable and names the token of every constructor parameter,
|
|
19
|
+
* in order.
|
|
20
|
+
*
|
|
21
|
+
* makeInjectable(RefParser, [{ multi: REF_TOKENS.Splitter }]);
|
|
22
|
+
*
|
|
23
|
+
* @param target - The class.
|
|
24
|
+
* @param parameters - The token of each constructor parameter, in order.
|
|
25
|
+
*/
|
|
26
|
+
export function makeInjectable(
|
|
27
|
+
target: new (...args: never[]) => unknown,
|
|
28
|
+
parameters: ConstructorParameter[] = []
|
|
29
|
+
): void {
|
|
30
|
+
decorate(injectable(), target);
|
|
31
|
+
parameters.forEach((parameter, index) => {
|
|
32
|
+
const decorator =
|
|
33
|
+
typeof parameter === "symbol" ? inject(parameter) : multiInject(parameter.multi);
|
|
34
|
+
decorate(decorator, target, index);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building a `RefLocation`: where a repository lives, in the form every ref
|
|
3
|
+
* and every machine-wide key agrees on.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { repoIdentity } from "../../lib/repos/identity.js";
|
|
7
|
+
import {
|
|
8
|
+
requireProvider,
|
|
9
|
+
type RepoProvider,
|
|
10
|
+
} from "../../lib/repos/providers/index.js";
|
|
11
|
+
import type { RefLocation } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where one repository lives, or undefined when its provider cannot take the
|
|
15
|
+
* path apart (a path of one segment, say).
|
|
16
|
+
*
|
|
17
|
+
* @param provider - The provider that reads the location.
|
|
18
|
+
* @param host - The repository's host.
|
|
19
|
+
* @param repoPath - The repository's path on that host.
|
|
20
|
+
*/
|
|
21
|
+
export function locationFor(
|
|
22
|
+
provider: RepoProvider,
|
|
23
|
+
host: string,
|
|
24
|
+
repoPath: string
|
|
25
|
+
): RefLocation | undefined {
|
|
26
|
+
const url = `https://${host}/${repoPath}`;
|
|
27
|
+
try {
|
|
28
|
+
return {
|
|
29
|
+
provider: provider.id,
|
|
30
|
+
host,
|
|
31
|
+
repoPath,
|
|
32
|
+
identity: repoIdentity(provider.canonicalize(url)),
|
|
33
|
+
url,
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where the repository at a configured URL lives, or undefined when no
|
|
42
|
+
* provider recognizes it. The location's `url` is the URL as the project
|
|
43
|
+
* configured it, which is what a person is shown.
|
|
44
|
+
*
|
|
45
|
+
* @param url - The repository's URL, as a project's config records it.
|
|
46
|
+
* @param providers - The providers to ask. Defaults to the built-ins.
|
|
47
|
+
*/
|
|
48
|
+
export function locationFromUrl(url: string, providers?: RepoProvider[]): RefLocation | undefined {
|
|
49
|
+
try {
|
|
50
|
+
const provider = requireProvider(url, undefined, providers);
|
|
51
|
+
const canonical = provider.canonicalize(url);
|
|
52
|
+
return {
|
|
53
|
+
provider: provider.id,
|
|
54
|
+
host: canonical.host,
|
|
55
|
+
repoPath: `${canonical.owner}/${canonical.name}`,
|
|
56
|
+
identity: repoIdentity(canonical),
|
|
57
|
+
url,
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { IndexFile } from "../../../lib/repos/formats/index-file.js";
|
|
2
|
+
import { locationFromUrl } from "../location.js";
|
|
3
|
+
import type { SousRef } from "../types.js";
|
|
4
|
+
import { CatalogMatcher, catalogRepoOfIndex, type CatalogRepo } from "./catalog-matcher.js";
|
|
5
|
+
import type { RefLookup, RefMatch } from "./ref-lookup.js";
|
|
6
|
+
|
|
7
|
+
/** What a `CachedIndexLookup` needs to know beyond the indexes themselves. */
|
|
8
|
+
export type CachedIndexLookupOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* The repositories' short names, in the order their matches should be
|
|
11
|
+
* listed: the built-in repository first, then the ones the config names.
|
|
12
|
+
* Defaults to the order the indexes were added to the map.
|
|
13
|
+
*/
|
|
14
|
+
order?: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Where each repository lives, by short name. It is how a ref written as a
|
|
17
|
+
* location (a URL, a locator, a URL copied from the browser) finds the
|
|
18
|
+
* repository whatever short name this project gave it.
|
|
19
|
+
*/
|
|
20
|
+
urls?: Record<string, string | undefined>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Answers repositories, namespaces and recipes from the repository indexes
|
|
25
|
+
* already on this machine. It reads nothing, fetches nothing and never fails:
|
|
26
|
+
* a candidate no index publishes simply has no match.
|
|
27
|
+
*/
|
|
28
|
+
export class CachedIndexLookup implements RefLookup {
|
|
29
|
+
private readonly matcher: CatalogMatcher;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param indexes - Each repository's cached index, keyed by short name.
|
|
33
|
+
* @param options - The listing order, and where each repository lives.
|
|
34
|
+
*/
|
|
35
|
+
constructor(indexes: Map<string, IndexFile>, options: CachedIndexLookupOptions = {}) {
|
|
36
|
+
const order = options.order ?? [...indexes.keys()];
|
|
37
|
+
const repos: CatalogRepo[] = [];
|
|
38
|
+
|
|
39
|
+
for (const name of order) {
|
|
40
|
+
const index = indexes.get(name);
|
|
41
|
+
if (index === undefined) continue;
|
|
42
|
+
const url = options.urls?.[name];
|
|
43
|
+
const location = url === undefined ? undefined : locationFromUrl(url);
|
|
44
|
+
|
|
45
|
+
repos.push(catalogRepoOfIndex(index, { name, ...(location === undefined ? {} : { location }) }));
|
|
46
|
+
}
|
|
47
|
+
this.matcher = new CatalogMatcher(repos);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async find(candidate: SousRef): Promise<RefMatch[]> {
|
|
51
|
+
return this.matcher.match(candidate);
|
|
52
|
+
}
|
|
53
|
+
}
|