@sous-io/sous 0.2.32 → 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 +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.md +8 -4
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +25 -17
- 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 +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
package/src/lib/repos/catalog.ts
CHANGED
|
@@ -27,23 +27,23 @@ import type { IndexDependency, IndexFile } from "./formats/index-file.js";
|
|
|
27
27
|
import type { Lockfile } from "./formats/lockfile.js";
|
|
28
28
|
import type { ContentKind, RecipeManifest } from "./formats/recipe-manifest.js";
|
|
29
29
|
import {
|
|
30
|
-
|
|
30
|
+
CachedIndexLookup,
|
|
31
|
+
RefResolveArguments,
|
|
32
|
+
describeRef,
|
|
33
|
+
isNamedRef,
|
|
34
|
+
locationOf,
|
|
35
|
+
namespaceOf,
|
|
31
36
|
namespaceOfKey,
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
repoOf,
|
|
38
|
+
settleDependency,
|
|
39
|
+
shortKey,
|
|
40
|
+
sharedRefResolver,
|
|
34
41
|
splitRecipeKey,
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
type NamedRef,
|
|
43
|
+
type RefKind,
|
|
44
|
+
type SousRef,
|
|
45
|
+
} from "../../services/ref-resolver/index.js";
|
|
38
46
|
import { declarationFor, type DependencyLists } from "./declarations.js";
|
|
39
|
-
import {
|
|
40
|
-
describeReference,
|
|
41
|
-
findNamespace,
|
|
42
|
-
findRecipe,
|
|
43
|
-
referenceReposFromIndexes,
|
|
44
|
-
type ReferenceContext,
|
|
45
|
-
type ReferenceMatch,
|
|
46
|
-
} from "../refs/index.js";
|
|
47
47
|
|
|
48
48
|
// --- What the catalog reads ---------------------------------------------------------------------
|
|
49
49
|
|
|
@@ -305,8 +305,11 @@ export function listRecipes(inputs: CatalogInputs): RecipeListing[] {
|
|
|
305
305
|
* @param inputs - The cached indexes, the lockfile and the project's subscriptions.
|
|
306
306
|
* @param ref - The namespace, optionally qualified with `repo:`.
|
|
307
307
|
*/
|
|
308
|
-
export function describeNamespace(
|
|
309
|
-
|
|
308
|
+
export async function describeNamespace(
|
|
309
|
+
inputs: CatalogInputs,
|
|
310
|
+
ref: string
|
|
311
|
+
): Promise<NamespaceDetail> {
|
|
312
|
+
const found = await resolveNamespaceRef(inputs, ref);
|
|
310
313
|
const subscriptions = new Set(inputs.subscriptions);
|
|
311
314
|
const declared = found.repo.index.namespaces[found.namespace]!;
|
|
312
315
|
|
|
@@ -329,8 +332,11 @@ export function describeNamespace(inputs: CatalogInputs, ref: string): Namespace
|
|
|
329
332
|
* @param inputs - The cached indexes, the lockfile, the subscriptions and the manifest reader.
|
|
330
333
|
* @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
|
|
331
334
|
*/
|
|
332
|
-
export function describeRecipe(
|
|
333
|
-
|
|
335
|
+
export async function describeRecipe(
|
|
336
|
+
inputs: CatalogInputs,
|
|
337
|
+
ref: string
|
|
338
|
+
): Promise<RecipeDetail> {
|
|
339
|
+
const found = await resolveRecipeRef(inputs, ref);
|
|
334
340
|
const subscriptions = new Set(inputs.subscriptions);
|
|
335
341
|
const listing = recipeListing(found.key, found.repo, inputs, subscriptions);
|
|
336
342
|
const entry = found.repo.index.recipes[found.key]!;
|
|
@@ -412,18 +418,18 @@ export function narrowToInstalled(inputs: CatalogInputs): CatalogInputs {
|
|
|
412
418
|
* @param describe - The lookup to run: `describeNamespace` or `describeRecipe`.
|
|
413
419
|
* @param what - What the ref names, for the error.
|
|
414
420
|
*/
|
|
415
|
-
export function describeInstalled<T>(
|
|
421
|
+
export async function describeInstalled<T>(
|
|
416
422
|
inputs: CatalogInputs,
|
|
417
423
|
ref: string,
|
|
418
|
-
describe: (inputs: CatalogInputs, ref: string) => T
|
|
424
|
+
describe: (inputs: CatalogInputs, ref: string) => Promise<T>,
|
|
419
425
|
what: "namespace" | "recipe"
|
|
420
|
-
): T {
|
|
426
|
+
): Promise<T> {
|
|
421
427
|
try {
|
|
422
|
-
return describe(narrowToInstalled(inputs), ref);
|
|
428
|
+
return await describe(narrowToInstalled(inputs), ref);
|
|
423
429
|
} catch {
|
|
424
430
|
// The full lookup either explains the ref better (ambiguous, unknown) or
|
|
425
431
|
// proves it is published and simply not installed.
|
|
426
|
-
describe(inputs, ref);
|
|
432
|
+
await describe(inputs, ref);
|
|
427
433
|
throw what === "namespace"
|
|
428
434
|
? new ConfigError(
|
|
429
435
|
`This project has installed no recipe from the namespace '${ref}', so there is ` +
|
|
@@ -464,35 +470,39 @@ export type ResolvedRecipeRef = {
|
|
|
464
470
|
* @param inputs - The cached indexes.
|
|
465
471
|
* @param ref - The namespace, optionally qualified with `repo:`.
|
|
466
472
|
*/
|
|
467
|
-
export function resolveNamespaceRef(
|
|
468
|
-
|
|
473
|
+
export async function resolveNamespaceRef(
|
|
474
|
+
inputs: CatalogInputs,
|
|
475
|
+
ref: string
|
|
476
|
+
): Promise<ResolvedNamespace> {
|
|
477
|
+
const parsed = readShortRef(ref);
|
|
469
478
|
|
|
470
|
-
if (parsed?.
|
|
479
|
+
if (parsed?.kind === "recipe") {
|
|
471
480
|
throw new ConfigError(
|
|
472
|
-
`'${ref}' names the recipe '${parsed.
|
|
473
|
-
` The namespace it belongs to is '${parsed.namespace}'.`
|
|
481
|
+
`'${ref}' names the recipe '${parsed.name}', not a namespace.\n` +
|
|
482
|
+
` The namespace it belongs to is '${parsed.namespace!.name}'.`
|
|
474
483
|
);
|
|
475
484
|
}
|
|
476
485
|
|
|
477
|
-
const matches =
|
|
486
|
+
const matches = await findIn(inputs, ref, ["namespace"]);
|
|
478
487
|
|
|
479
488
|
if (matches.length === 1) {
|
|
480
|
-
|
|
489
|
+
const match = matches[0]!;
|
|
490
|
+
return { repo: repoNamed(inputs, repoOf(match)!.name!), namespace: namespaceOf(match)!.name };
|
|
481
491
|
}
|
|
482
492
|
|
|
483
493
|
if (matches.length > 1) throw ambiguousError(ref, "namespace", matches);
|
|
484
494
|
|
|
485
|
-
const asRecipe =
|
|
486
|
-
if (parsed !== undefined && parsed
|
|
495
|
+
const asRecipe = await findIn(inputs, ref, ["recipe"]);
|
|
496
|
+
if (parsed !== undefined && repoOf(parsed) === undefined && asRecipe.length > 0) {
|
|
487
497
|
throw new ConfigError(
|
|
488
498
|
`No repository this project trusts publishes a namespace called ` +
|
|
489
|
-
`'${parsed.
|
|
499
|
+
`'${parsed.name}'.\n` +
|
|
490
500
|
` It is the name of a recipe:\n` +
|
|
491
|
-
asRecipe.map((candidate) => ` ${
|
|
501
|
+
asRecipe.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
|
|
492
502
|
);
|
|
493
503
|
}
|
|
494
504
|
|
|
495
|
-
throw unknownError(inputs, ref, "namespace", parsed?.
|
|
505
|
+
throw unknownError(inputs, ref, "namespace", parsed === undefined ? undefined : repoOf(parsed)?.name);
|
|
496
506
|
}
|
|
497
507
|
|
|
498
508
|
/**
|
|
@@ -503,35 +513,39 @@ export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): Resolve
|
|
|
503
513
|
* @param inputs - The cached indexes.
|
|
504
514
|
* @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
|
|
505
515
|
*/
|
|
506
|
-
export function resolveRecipeRef(
|
|
507
|
-
|
|
508
|
-
|
|
516
|
+
export async function resolveRecipeRef(
|
|
517
|
+
inputs: CatalogInputs,
|
|
518
|
+
ref: string
|
|
519
|
+
): Promise<ResolvedRecipeRef> {
|
|
520
|
+
const parsed = readShortRef(ref);
|
|
509
521
|
|
|
510
|
-
const matches =
|
|
522
|
+
const matches = await findIn(inputs, ref, ["recipe"]);
|
|
511
523
|
|
|
512
524
|
if (matches.length === 1) {
|
|
513
525
|
const match = matches[0]!;
|
|
526
|
+
const namespace = namespaceOf(match)!.name;
|
|
527
|
+
const name = (match as { name: string }).name;
|
|
514
528
|
return {
|
|
515
|
-
repo: repoNamed(inputs, match
|
|
516
|
-
key: `${
|
|
517
|
-
namespace
|
|
518
|
-
name
|
|
529
|
+
repo: repoNamed(inputs, repoOf(match)!.name!),
|
|
530
|
+
key: `${namespace}/${name}`,
|
|
531
|
+
namespace,
|
|
532
|
+
name,
|
|
519
533
|
};
|
|
520
534
|
}
|
|
521
535
|
|
|
522
536
|
if (matches.length > 1) throw ambiguousError(ref, "recipe", matches);
|
|
523
537
|
|
|
524
|
-
const asNamespace =
|
|
538
|
+
const asNamespace = await findIn(inputs, ref, ["namespace"]);
|
|
525
539
|
if (asNamespace.length > 0) {
|
|
526
540
|
throw new ConfigError(
|
|
527
541
|
`No repository this project trusts publishes a recipe called ` +
|
|
528
|
-
`'${parsed
|
|
542
|
+
`'${parsed === undefined ? ref : namespaceNameOf(parsed)}'.\n` +
|
|
529
543
|
` It is the name of a namespace:\n` +
|
|
530
|
-
asNamespace.map((candidate) => ` ${
|
|
544
|
+
asNamespace.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
|
|
531
545
|
);
|
|
532
546
|
}
|
|
533
547
|
|
|
534
|
-
throw unknownError(inputs, ref, "recipe", parsed?.
|
|
548
|
+
throw unknownError(inputs, ref, "recipe", parsed === undefined ? undefined : repoOf(parsed)?.name);
|
|
535
549
|
}
|
|
536
550
|
|
|
537
551
|
// --- The pieces ---------------------------------------------------------------------------------
|
|
@@ -711,7 +725,7 @@ function dependencyListings(
|
|
|
711
725
|
const reading = settleDependency(written, {
|
|
712
726
|
...(resolved === undefined ? {} : { recorded: resolved }),
|
|
713
727
|
});
|
|
714
|
-
if (reading !== undefined &&
|
|
728
|
+
if (reading !== undefined && isNamedRef(reading)) key = shortKey(reading);
|
|
715
729
|
} catch {
|
|
716
730
|
key = written;
|
|
717
731
|
}
|
|
@@ -733,45 +747,57 @@ function repoNamed(inputs: CatalogInputs, name: string): CatalogRepo {
|
|
|
733
747
|
return inputs.repos.find((repo) => repo.name === name)!;
|
|
734
748
|
}
|
|
735
749
|
|
|
736
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
750
|
+
/**
|
|
751
|
+
* Every ref in this catalog's repositories that a ref written on the command
|
|
752
|
+
* line names, of the kinds given, best match first.
|
|
753
|
+
*
|
|
754
|
+
* @param inputs - The cached indexes.
|
|
755
|
+
* @param ref - The ref as typed.
|
|
756
|
+
* @param kinds - The kinds of ref the caller accepts.
|
|
757
|
+
*/
|
|
758
|
+
async function findIn(
|
|
759
|
+
inputs: CatalogInputs,
|
|
760
|
+
ref: string,
|
|
761
|
+
kinds: readonly RefKind[]
|
|
762
|
+
): Promise<SousRef[]> {
|
|
763
|
+
const lookup = new CachedIndexLookup(
|
|
764
|
+
new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
|
|
765
|
+
{
|
|
766
|
+
order: inputs.repos.map((repo) => repo.name),
|
|
767
|
+
urls: Object.fromEntries(inputs.repos.map((repo) => [repo.name, repo.url])),
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
const result = await sharedRefResolver().resolve(new RefResolveArguments({ input: ref, lookup, kinds }));
|
|
771
|
+
return result.refs;
|
|
745
772
|
}
|
|
746
773
|
|
|
747
774
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* says which version to use, never which thing is meant), or a location
|
|
751
|
-
* exactly as written, which the search settles through the repository's index.
|
|
775
|
+
* The namespace or recipe a ref from the command line names by its short form,
|
|
776
|
+
* checked by the one parser: undefined when the ref names a location instead.
|
|
752
777
|
*
|
|
753
778
|
* @param ref - The ref as typed.
|
|
754
|
-
* @returns The short reading, when the ref was short, and the search term.
|
|
755
779
|
*/
|
|
756
|
-
function
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
780
|
+
function readShortRef(ref: string): NamedRef | undefined {
|
|
781
|
+
const { refs } = sharedRefResolver().parse(ref);
|
|
782
|
+
return refs.find(
|
|
783
|
+
(candidate): candidate is NamedRef => isNamedRef(candidate) && locationOf(candidate) === undefined
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** The name a short ref was written with: its namespace for a namespace, its recipe name otherwise. */
|
|
788
|
+
function namespaceNameOf(ref: NamedRef): string {
|
|
789
|
+
return ref.kind === "namespace" ? ref.name : ref.namespace!.name;
|
|
764
790
|
}
|
|
765
791
|
|
|
766
792
|
/** The error a ref that could have meant several things raises. */
|
|
767
793
|
function ambiguousError(
|
|
768
794
|
ref: string,
|
|
769
795
|
what: "namespace" | "recipe",
|
|
770
|
-
candidates:
|
|
796
|
+
candidates: SousRef[]
|
|
771
797
|
): ConfigError {
|
|
772
798
|
return new ConfigError(
|
|
773
799
|
`'${ref}' names a ${what} in more than one repository this project trusts:\n` +
|
|
774
|
-
candidates.map((candidate) => ` ${
|
|
800
|
+
candidates.map((candidate) => ` ${describeRef(candidate)}`).join("\n") +
|
|
775
801
|
`\n Name the repository as well, as 'repository:${ref}', to say which one you mean.`
|
|
776
802
|
);
|
|
777
803
|
}
|
|
@@ -14,9 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
import type { DependencyKind } from "./formats/common.js";
|
|
16
16
|
import type { IndexVersion } from "./formats/index-file.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import {
|
|
18
|
+
RefSource,
|
|
19
|
+
isNamedRef,
|
|
20
|
+
locationOf,
|
|
21
|
+
namespaceOfKey,
|
|
22
|
+
settleDependency,
|
|
23
|
+
shortKey,
|
|
24
|
+
sharedRefResolver,
|
|
25
|
+
type RecordedDependencies,
|
|
26
|
+
} from "../../services/ref-resolver/index.js";
|
|
20
27
|
|
|
21
28
|
/** A manifest's two dependency lists, each entry exactly as it was written. */
|
|
22
29
|
export type DependencyLists = {
|
|
@@ -112,10 +119,12 @@ export function declarationFor(
|
|
|
112
119
|
// validated; here it simply covers nothing.
|
|
113
120
|
continue;
|
|
114
121
|
}
|
|
115
|
-
if (reading === undefined || !
|
|
122
|
+
if (reading === undefined || !isNamedRef(reading)) continue;
|
|
116
123
|
|
|
117
|
-
const named = reading.
|
|
118
|
-
const covers = named
|
|
124
|
+
const named = reading.kind === "recipe";
|
|
125
|
+
const covers = named
|
|
126
|
+
? shortKey(reading) === key
|
|
127
|
+
: namespaceOfKey(key) === reading.name;
|
|
119
128
|
if (covers) gathered = foldDeclaration(gathered, { written, kind, named });
|
|
120
129
|
}
|
|
121
130
|
}
|
|
@@ -152,12 +161,13 @@ export function indexDependencyLists(
|
|
|
152
161
|
*/
|
|
153
162
|
export function describeDeclaration(declared: string): string {
|
|
154
163
|
try {
|
|
155
|
-
const
|
|
156
|
-
const only =
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
const { refs } = sharedRefResolver().parse(declared, RefSource.Manifest);
|
|
165
|
+
const only = refs[0]!;
|
|
166
|
+
if (refs.length === 1 && only.kind === "namespace") {
|
|
167
|
+
const location = locationOf(only);
|
|
168
|
+
return location === undefined
|
|
169
|
+
? `the whole '${only.name}' namespace`
|
|
170
|
+
: `the whole '${only.name}' namespace of ${location.identity}`;
|
|
161
171
|
}
|
|
162
172
|
} catch {
|
|
163
173
|
// Shown as it was written; the reader sees the bad entry.
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
stableJsonStringify,
|
|
34
34
|
} from "./common.js";
|
|
35
35
|
import { publishedVariableDefinitionSchema } from "./variable-definition.js";
|
|
36
|
-
import { namespaceOfKey } from "
|
|
36
|
+
import { namespaceOfKey } from "../../../services/ref-resolver/index.js";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* One dependency of one published version, as the release resolved it.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Every other location form works too (an HTTPS or SSH URL, a browser URL); a
|
|
24
24
|
* short name such as `sous-recipes:` is a consuming project's own label, so it
|
|
25
|
-
* never appears in a published manifest. See `
|
|
25
|
+
* never appears in a published manifest. See `RefResolverService.parse` with
|
|
26
26
|
* `RefSource.Manifest`.
|
|
27
27
|
*/
|
|
28
28
|
|
|
@@ -37,8 +37,7 @@ import {
|
|
|
37
37
|
semverVersionSchema,
|
|
38
38
|
submissionsSchema,
|
|
39
39
|
} from "./common.js";
|
|
40
|
-
import {
|
|
41
|
-
import { RefSource } from "../../refs/scopes.js";
|
|
40
|
+
import { RefSource, sharedRefResolver } from "../../../services/ref-resolver/index.js";
|
|
42
41
|
import { variableDefinitionSchema } from "./variable-definition.js";
|
|
43
42
|
|
|
44
43
|
// Variable definitions live in their own module, so the index can read them
|
|
@@ -56,7 +55,7 @@ export * from "./variable-definition.js";
|
|
|
56
55
|
*/
|
|
57
56
|
const dependencyRefSchema = z.string().superRefine((value, ctx) => {
|
|
58
57
|
try {
|
|
59
|
-
|
|
58
|
+
sharedRefResolver().parse(value, RefSource.Manifest);
|
|
60
59
|
} catch (error) {
|
|
61
60
|
ctx.addIssue({ code: "custom", message: (error as Error).message });
|
|
62
61
|
}
|
|
@@ -34,7 +34,7 @@ import type { ResolvedRecipe } from "./resolver.js";
|
|
|
34
34
|
import type { RecipeStoreLike, StoreKey } from "./store/contract.js";
|
|
35
35
|
import { builtInProviders, requireProvider } from "./providers/index.js";
|
|
36
36
|
import { repoIdentity } from "./identity.js";
|
|
37
|
-
import { splitRecipeKey } from "
|
|
37
|
+
import { splitRecipeKey } from "../../services/ref-resolver/index.js";
|
|
38
38
|
import type { ProviderOptions, RepoProvider } from "./providers/provider.js";
|
|
39
39
|
|
|
40
40
|
/** What the lockfile needs to know about a repository. */
|
|
@@ -24,15 +24,19 @@
|
|
|
24
24
|
* can tell.
|
|
25
25
|
*
|
|
26
26
|
* A project with no lockfile entries gets no resolver at all, so a project that
|
|
27
|
-
* uses no repositories behaves exactly as it did before: `~` in an
|
|
28
|
-
*
|
|
27
|
+
* uses no repositories behaves exactly as it did before: a `~` reference in an
|
|
28
|
+
* include line is just a relative path.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import semver from "semver";
|
|
32
32
|
import type { Settings } from "../settings.js";
|
|
33
33
|
import { resolveStoreRoot } from "../sous-home.js";
|
|
34
|
-
import {
|
|
35
|
-
|
|
34
|
+
import {
|
|
35
|
+
RefSource,
|
|
36
|
+
namespaceOfKey,
|
|
37
|
+
parseNamedRef,
|
|
38
|
+
shortKey,
|
|
39
|
+
} from "../../services/ref-resolver/index.js";
|
|
36
40
|
import {
|
|
37
41
|
StaticNamespaceResolver,
|
|
38
42
|
type DroppedRecipe,
|
|
@@ -64,7 +68,8 @@ export type ProjectNamespaceResolverOptions = {
|
|
|
64
68
|
* locks no recipes at all.
|
|
65
69
|
*
|
|
66
70
|
* createProjectNamespaceResolver({ sousDir, settings })
|
|
67
|
-
* // -> resolves "@~workflow/task-files/_partials/resume.md"
|
|
71
|
+
* // -> resolves "@~workflow/task-files/_partials/resume.md" (or a glob such as
|
|
72
|
+
* // "@~*" + "/*" + "/memories/*.md") to the pinned
|
|
68
73
|
* // recipe directory, when the project (or the including recipe) may address it
|
|
69
74
|
*
|
|
70
75
|
* @param options - The project's `.sous/` directory, its config and the environment.
|
|
@@ -82,10 +87,12 @@ export function createProjectNamespaceResolver(
|
|
|
82
87
|
if (locked.length === 0) return undefined;
|
|
83
88
|
|
|
84
89
|
const recipes: Record<string, string> = {};
|
|
90
|
+
const repos: Record<string, string> = {};
|
|
85
91
|
const dependencies: Record<string, string[]> = {};
|
|
86
92
|
|
|
87
93
|
for (const recipe of locked) {
|
|
88
94
|
recipes[recipe.key] = recipe.dir;
|
|
95
|
+
repos[recipe.key] = recipe.repo;
|
|
89
96
|
|
|
90
97
|
// Every recipe holding this one declared it, in `depends` or `subscribes`,
|
|
91
98
|
// and the lockfile records the key that declaration resolved to.
|
|
@@ -98,6 +105,7 @@ export function createProjectNamespaceResolver(
|
|
|
98
105
|
// lockfile pins, and those are exactly the recipes this resolver knows.
|
|
99
106
|
return new StaticNamespaceResolver({
|
|
100
107
|
recipes,
|
|
108
|
+
repos,
|
|
101
109
|
dependencies,
|
|
102
110
|
explainMissing: (recipe) => explainUnpinnedRecipe(recipe, options),
|
|
103
111
|
});
|
|
@@ -131,7 +139,7 @@ export function explainUnpinnedRecipe(
|
|
|
131
139
|
if (entry?.enabled !== false) continue;
|
|
132
140
|
let ref: string;
|
|
133
141
|
try {
|
|
134
|
-
ref =
|
|
142
|
+
ref = shortKey(parseNamedRef(key, RefSource.Config));
|
|
135
143
|
} catch {
|
|
136
144
|
// The config schema already refuses a key that is not a ref.
|
|
137
145
|
continue;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an include line may name: the recipes the lockfile pins.
|
|
3
|
+
*
|
|
4
|
+
* A `RefLookup` over the pinned recipes, answering a `recipeFile` reading (the
|
|
5
|
+
* namespace and recipe names, and the path inside the recipe) with every pinned
|
|
6
|
+
* recipe whose names match it. Names are compared as the ref service compares
|
|
7
|
+
* every name: exact spelling first, ignoring case as the fallback, and a name
|
|
8
|
+
* may be a glob (`*`, `**`, `?`, `[..]`, `{a,b}`). The path inside the recipe
|
|
9
|
+
* is carried through as written; expanding it into files is the include
|
|
10
|
+
* resolver's job, because only it knows which directory each recipe lives in.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
compareBytewise,
|
|
15
|
+
matchName,
|
|
16
|
+
repoOf,
|
|
17
|
+
type NameMatch,
|
|
18
|
+
type RefMatch,
|
|
19
|
+
type SousRef,
|
|
20
|
+
type SyncRefLookup,
|
|
21
|
+
} from "../../services/ref-resolver/index.js";
|
|
22
|
+
|
|
23
|
+
/** One pinned recipe, as far as a lookup needs to know it. */
|
|
24
|
+
export type RecipeFileEntry = {
|
|
25
|
+
/** The recipe's namespace. */
|
|
26
|
+
namespace: string;
|
|
27
|
+
/** The recipe's name. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** The short name of the repository it came from, when known. */
|
|
30
|
+
repo?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The weakest of two matches: a name that matched only ignoring case spoils an exact one. */
|
|
34
|
+
function combine(left: NameMatch, right: NameMatch): NameMatch {
|
|
35
|
+
if (left === undefined || right === undefined) return undefined;
|
|
36
|
+
return left === "exact" && right === "exact" ? "exact" : "folded";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Answers which pinned recipes an include reading names. */
|
|
40
|
+
export class LockedRecipeFileLookup implements SyncRefLookup {
|
|
41
|
+
/**
|
|
42
|
+
* @param entries - Every recipe the lockfile pins.
|
|
43
|
+
*/
|
|
44
|
+
constructor(private readonly entries: readonly RecipeFileEntry[]) {}
|
|
45
|
+
|
|
46
|
+
/** The distinct namespaces of the pinned recipes, sorted. */
|
|
47
|
+
namespaces(): string[] {
|
|
48
|
+
return [...new Set(this.entries.map((entry) => entry.namespace))].sort(compareBytewise);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The pinned recipes of the namespaces a written namespace matches, sorted
|
|
53
|
+
* by key: those whose spelling matches exactly, or, when none does, those
|
|
54
|
+
* that differ only in case.
|
|
55
|
+
*
|
|
56
|
+
* @param written - The namespace as written, possibly a glob.
|
|
57
|
+
*/
|
|
58
|
+
recipesInNamespace(written: string): string[] {
|
|
59
|
+
const matched = this.entries
|
|
60
|
+
.map((entry) => ({ entry, match: matchName(written, entry.namespace) }))
|
|
61
|
+
.filter((hit) => hit.match !== undefined);
|
|
62
|
+
const exact = matched.filter((hit) => hit.match === "exact");
|
|
63
|
+
return (exact.length > 0 ? exact : matched)
|
|
64
|
+
.map((hit) => `${hit.entry.namespace}/${hit.entry.name}`)
|
|
65
|
+
.sort(compareBytewise);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True when a written namespace matches a pinned one in some spelling.
|
|
70
|
+
*
|
|
71
|
+
* @param written - The namespace as written, possibly a glob.
|
|
72
|
+
*/
|
|
73
|
+
knowsNamespace(written: string): boolean {
|
|
74
|
+
return this.entries.some((entry) => matchName(written, entry.namespace) !== undefined);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
findSync(candidate: SousRef): RefMatch[] {
|
|
78
|
+
if (candidate.kind !== "recipeFile") return [];
|
|
79
|
+
const written = candidate.recipe;
|
|
80
|
+
const namespaceName = written.namespace?.name;
|
|
81
|
+
if (namespaceName === undefined) return [];
|
|
82
|
+
const repoName = repoOf(candidate)?.name;
|
|
83
|
+
|
|
84
|
+
const found: RefMatch[] = [];
|
|
85
|
+
for (const entry of this.entries) {
|
|
86
|
+
let match = combine(
|
|
87
|
+
matchName(namespaceName, entry.namespace),
|
|
88
|
+
matchName(written.name, entry.name)
|
|
89
|
+
);
|
|
90
|
+
if (repoName !== undefined) {
|
|
91
|
+
match = combine(
|
|
92
|
+
match,
|
|
93
|
+
entry.repo === undefined ? undefined : matchName(repoName, entry.repo)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (match === undefined) continue;
|
|
97
|
+
found.push({
|
|
98
|
+
ref: {
|
|
99
|
+
kind: "recipeFile",
|
|
100
|
+
path: candidate.path,
|
|
101
|
+
recipe: {
|
|
102
|
+
kind: "recipe",
|
|
103
|
+
name: entry.name,
|
|
104
|
+
namespace: { kind: "namespace", name: entry.namespace },
|
|
105
|
+
},
|
|
106
|
+
...(candidate.glob === undefined ? {} : { glob: true as const }),
|
|
107
|
+
...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
|
|
108
|
+
},
|
|
109
|
+
exactSpelling: match === "exact",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return found.sort((left, right) => compareBytewise(keyOf(left.ref), keyOf(right.ref)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async find(candidate: SousRef): Promise<RefMatch[]> {
|
|
116
|
+
return this.findSync(candidate);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The `namespace/recipe` key of a recipe file ref, for ordering. */
|
|
121
|
+
function keyOf(ref: SousRef): string {
|
|
122
|
+
return ref.kind === "recipeFile" ? `${ref.recipe.namespace?.name}/${ref.recipe.name}` : "";
|
|
123
|
+
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./load-manifest.js";
|
|
34
34
|
import { readEffectiveLinks } from "./links.js";
|
|
35
35
|
import { identitySegments } from "./identity.js";
|
|
36
|
-
import { splitRecipeKey } from "
|
|
36
|
+
import { splitRecipeKey } from "../../services/ref-resolver/index.js";
|
|
37
37
|
import { PROJECT_HOLDER } from "./formats/lockfile.js";
|
|
38
38
|
|
|
39
39
|
/** One locked recipe, together with the directory its files are read from. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `#memories` view: every memory an active recipe publishes, at
|
|
3
|
+
* `#memories/<namespace>/<recipe>/<path>`. See `recipe-memories.ts` for what is
|
|
4
|
+
* listed and in which order.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { makeInjectable } from "../../services/ref-resolver/injectable.js";
|
|
8
|
+
import type { HashName, HashViewContext, ViewFile } from "../../services/ref-resolver/hash-names.js";
|
|
9
|
+
import { listMemories } from "./recipe-memories.js";
|
|
10
|
+
|
|
11
|
+
/** The `#memories` view. */
|
|
12
|
+
export class MemoriesHashName implements HashName {
|
|
13
|
+
readonly name = "memories";
|
|
14
|
+
readonly registeredBy = "sous";
|
|
15
|
+
readonly description =
|
|
16
|
+
"the memories of every active recipe, listed in dependency order as #memories/<namespace>/<recipe>/<path>.";
|
|
17
|
+
|
|
18
|
+
bases(): string[] {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
view(context: HashViewContext): ViewFile[] {
|
|
23
|
+
const memories = context.settings.recipes?.memories;
|
|
24
|
+
return listMemories({
|
|
25
|
+
sousDir: context.sousDir,
|
|
26
|
+
...(context.env === undefined ? {} : { env: context.env }),
|
|
27
|
+
first: memories?.first,
|
|
28
|
+
exclude: memories?.exclude,
|
|
29
|
+
}).map((memory) => ({ path: memory.path, file: memory.file }));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
makeInjectable(MemoriesHashName);
|