@sous-io/sous 0.2.31 → 0.2.33
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 +2 -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.md +14 -9
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +32 -17
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +8 -2
- 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 +29 -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 +85 -11
- package/src/lib/config-schema.ts +59 -19
- package/src/lib/include-resolver.ts +307 -69
- package/src/lib/markdown-compiler.ts +273 -81
- 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 +27 -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/ask.ts +3 -3
- package/src/lib/vars/mappings.ts +17 -9
- 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 +25 -18
- 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,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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SousRef } from "../types.js";
|
|
2
|
+
import { CatalogMatcher, type CatalogRepo } from "./catalog-matcher.js";
|
|
3
|
+
import type { RefLookup, RefMatch } from "./ref-lookup.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Answers repositories, namespaces and recipes from a catalog a caller has
|
|
7
|
+
* already built (which repositories it knows, and what each publishes). It
|
|
8
|
+
* reads nothing, fetches nothing and never fails.
|
|
9
|
+
*/
|
|
10
|
+
export class CatalogLookup implements RefLookup {
|
|
11
|
+
private readonly matcher: CatalogMatcher;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param repos - The repositories to search, in the order their matches are listed.
|
|
15
|
+
*/
|
|
16
|
+
constructor(repos: CatalogRepo[]) {
|
|
17
|
+
this.matcher = new CatalogMatcher(repos);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async find(candidate: SousRef): Promise<RefMatch[]> {
|
|
21
|
+
return this.matcher.match(candidate);
|
|
22
|
+
}
|
|
23
|
+
}
|