@sous-io/sous 0.2.37 → 1.0.2
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 +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1094 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { escape as escapeGlob } from "glob";
|
|
3
|
-
import { braceExpand } from "minimatch";
|
|
4
|
-
import {
|
|
5
|
-
RefResolveArguments,
|
|
6
|
-
RefSource,
|
|
7
|
-
namespaceOfKey,
|
|
8
|
-
splitRecipeKey,
|
|
9
|
-
sharedRefResolver,
|
|
10
|
-
splitSegments,
|
|
11
|
-
type RecipeFileRef,
|
|
12
|
-
} from "../../services/ref-resolver/index.js";
|
|
13
|
-
import { LockedRecipeFileLookup } from "./locked-recipe-lookup.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Namespace addressability for templates: the reserved `~` include sigil.
|
|
17
|
-
*
|
|
18
|
-
* An include line of the form `@~<namespace>/<recipe>/<path>` (and the
|
|
19
|
-
* equivalent `{% render "~<namespace>/<recipe>/<path>" %}`) addresses a recipe
|
|
20
|
-
* rather than the filesystem. The reference is read by the ref resolver service
|
|
21
|
-
* (`RefSource.Include`) and matched against the recipes the project pins, so any
|
|
22
|
-
* form that service allows works: a glob in any name or in the path, a `repo:`
|
|
23
|
-
* qualifier, a name in any case. For example
|
|
24
|
-
*
|
|
25
|
-
* @~workflow/task-files/_partials/resume.md
|
|
26
|
-
*
|
|
27
|
-
* means "the file `_partials/resume.md` inside recipe `workflow/task-files`".
|
|
28
|
-
* A namespace holds many recipes and each recipe is a directory at its pinned
|
|
29
|
-
* version, so resolution is a lookup from (namespace, recipe) to a directory.
|
|
30
|
-
*
|
|
31
|
-
* A bare `@path` (no `~`) never consults a namespace; it stays a relative path
|
|
32
|
-
* or a declared alias.
|
|
33
|
-
*
|
|
34
|
-
* This module defines only the CONTRACT plus a static, in-memory implementation
|
|
35
|
-
* used by tests. The real implementation (backed by the repository store, the
|
|
36
|
-
* lockfile and linked checkouts) is supplied by the repositories layer and is
|
|
37
|
-
* injected into the compiler, so nothing here reads configuration or disk.
|
|
38
|
-
*
|
|
39
|
-
* Scoping is the resolver's responsibility, which is why every request carries
|
|
40
|
-
* the including file:
|
|
41
|
-
* - a file that lives inside a recipe may address only that recipe's declared
|
|
42
|
-
* dependencies (`depends` plus `subscribes`) at their pinned versions;
|
|
43
|
-
* - a file in the project's own templates may address every recipe the
|
|
44
|
-
* project's lockfile pins, whatever holds it.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Why a recipe a project template asked for is not pinned any more, when that
|
|
49
|
-
* can be known. Only ever attached to an answer for a project template.
|
|
50
|
-
*/
|
|
51
|
-
export type DroppedRecipe =
|
|
52
|
-
/**
|
|
53
|
-
* Another version of a pinned recipe declares it (in `depends` or
|
|
54
|
-
* `subscribes`), and the version the lockfile pins does not: the usual story
|
|
55
|
-
* of a set that dropped one of its members.
|
|
56
|
-
*/
|
|
57
|
-
| {
|
|
58
|
-
by: "recipe";
|
|
59
|
-
recipe: string;
|
|
60
|
-
declaredAt: string;
|
|
61
|
-
pinned: string;
|
|
62
|
-
/** The recipes, no longer pinned either, that the chain ran through, in order. */
|
|
63
|
-
through?: string[];
|
|
64
|
-
}
|
|
65
|
-
/** The project subscribes to it, but the subscription is switched off. */
|
|
66
|
-
| { by: "disabled-subscription"; subscription: string };
|
|
67
|
-
|
|
68
|
-
/** A recipe a candidate belongs to: its key and its directory. */
|
|
69
|
-
export type RecipeRoot = { recipe: string; dir: string };
|
|
70
|
-
|
|
71
|
-
/** A single `~` reference resolution request. */
|
|
72
|
-
export type NamespaceRequest = {
|
|
73
|
-
/**
|
|
74
|
-
* The reference with its `~` sigil taken off: namespace, recipe and the path
|
|
75
|
-
* inside the recipe (e.g. `workflow/task-files/_partials/resume.md`), as
|
|
76
|
-
* written, possibly with a `repo:` qualifier, globs and a `?name=value` query.
|
|
77
|
-
*/
|
|
78
|
-
reference: string;
|
|
79
|
-
/**
|
|
80
|
-
* Absolute path of the file performing the include, used to decide which
|
|
81
|
-
* recipe (if any) is asking. A directory path is accepted for callers that
|
|
82
|
-
* only know the including directory, such as the `{% render %}` filesystem.
|
|
83
|
-
*/
|
|
84
|
-
fromFile: string;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The outcome of a namespace lookup. `candidates` is the success case; the
|
|
89
|
-
* other members carry enough detail to build a precise error message.
|
|
90
|
-
*
|
|
91
|
-
* Implementations may only return these members. Callers should treat any
|
|
92
|
-
* unrecognized `kind` as "no candidates, no specific advice" so the union can
|
|
93
|
-
* grow without breaking older callers.
|
|
94
|
-
*/
|
|
95
|
-
export type NamespaceResolution =
|
|
96
|
-
/**
|
|
97
|
-
* Ordered absolute paths to try, most preferred first. An empty list means
|
|
98
|
-
* the lookup produced nothing. When `glob` is true the reference was a glob:
|
|
99
|
-
* every candidate is a pattern (the recipe's own directory escaped), and ALL
|
|
100
|
-
* of them are included, in order, not just the first that exists.
|
|
101
|
-
* `roots`, when present, holds the recipe (and its directory) each candidate belongs to
|
|
102
|
-
* (same length and order); a caller that reads files refuses any file whose
|
|
103
|
-
* real path is outside its root, which is how a symlink inside a recipe cannot
|
|
104
|
-
* lead out of it.
|
|
105
|
-
*/
|
|
106
|
-
| { kind: "candidates"; candidates: string[]; glob?: true; roots?: RecipeRoot[] }
|
|
107
|
-
/**
|
|
108
|
-
* No such namespace is known at all. `known` lists the namespaces that are.
|
|
109
|
-
* `dropped` says why a project template's recipe is no longer pinned, when
|
|
110
|
-
* that can be known.
|
|
111
|
-
*/
|
|
112
|
-
| { kind: "unknown-namespace"; namespace: string; recipe: string; known: string[]; dropped?: DroppedRecipe }
|
|
113
|
-
/**
|
|
114
|
-
* The namespace exists but holds no such recipe. `recipe` is the fully
|
|
115
|
-
* qualified ref that was asked for; `known` lists the recipe refs the
|
|
116
|
-
* namespace does hold; `dropped` is as above.
|
|
117
|
-
*/
|
|
118
|
-
| { kind: "unknown-recipe"; recipe: string; known: string[]; dropped?: DroppedRecipe }
|
|
119
|
-
/**
|
|
120
|
-
* The recipe exists but the including file is not allowed to address it.
|
|
121
|
-
* `recipe` is the fully qualified ref that was asked for. `includingRecipe`
|
|
122
|
-
* is the ref of the recipe the including file belongs to, or `null` when the
|
|
123
|
-
* including file is one of the project's own templates (which only happens
|
|
124
|
-
* when the resolver was given a narrower `projectScope`).
|
|
125
|
-
*/
|
|
126
|
-
| { kind: "not-a-dependency"; recipe: string; includingRecipe: string | null }
|
|
127
|
-
/**
|
|
128
|
-
* The reference tried to leave the recipe directory: it carried a `.` or `..`
|
|
129
|
-
* segment, or an absolute inner path. A `~namespace` reference addresses a
|
|
130
|
-
* recipe's own files and nothing else, so this is refused rather than
|
|
131
|
-
* resolved. `reference` is the reference as it was written.
|
|
132
|
-
*/
|
|
133
|
-
| { kind: "escapes-recipe"; recipe: string; reference: string }
|
|
134
|
-
/**
|
|
135
|
-
* The reference is not one an include line may hold (it names no file inside
|
|
136
|
-
* a recipe, carries a version range, and so on). `message` is the ref
|
|
137
|
-
* service's own sentence, which says what to write instead.
|
|
138
|
-
*/
|
|
139
|
-
| { kind: "invalid"; reference: string; message: string };
|
|
140
|
-
|
|
141
|
-
/** Resolves `~` references to candidate absolute paths. */
|
|
142
|
-
export interface NamespaceResolver {
|
|
143
|
-
/**
|
|
144
|
-
* Resolve one `~` reference.
|
|
145
|
-
*
|
|
146
|
-
* @param request - The reference and the including file.
|
|
147
|
-
* @returns Candidate absolute paths (most preferred first), or a reason the lookup failed.
|
|
148
|
-
*/
|
|
149
|
-
resolve(request: NamespaceRequest): NamespaceResolution;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* The ref of the recipe whose directory holds `fromFile`, or null when the
|
|
153
|
-
* file is not inside any recipe (a project template). Optional: a resolver
|
|
154
|
-
* that cannot tell is treated as one for which every file is the project's.
|
|
155
|
-
*
|
|
156
|
-
* @param fromFile - Absolute path of a file or directory.
|
|
157
|
-
*/
|
|
158
|
-
includingRecipe?(fromFile: string): string | null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Render a namespace lookup failure as human-readable lines for an error
|
|
163
|
-
* message. Each line is indented by two spaces so it can be appended directly
|
|
164
|
-
* to the compiler's "Include not found" block.
|
|
165
|
-
*
|
|
166
|
-
* @param opts.fromFile - The file (or directory) that performed the include.
|
|
167
|
-
* @param opts.resolution - What the resolver returned.
|
|
168
|
-
* @returns Indented, newline-joined explanation lines; an empty string when there is nothing to add.
|
|
169
|
-
*/
|
|
170
|
-
export function formatNamespaceProblem(opts: {
|
|
171
|
-
fromFile: string;
|
|
172
|
-
resolution: NamespaceResolution;
|
|
173
|
-
}): string {
|
|
174
|
-
const resolution = opts.resolution;
|
|
175
|
-
|
|
176
|
-
if (resolution.kind === "candidates") {
|
|
177
|
-
return "";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (resolution.kind === "invalid") {
|
|
181
|
-
return [`in file: ${opts.fromFile}`, `reference: ~${resolution.reference}`, ...resolution.message.split("\n")]
|
|
182
|
-
.map((line) => ` ${line}`)
|
|
183
|
-
.join("\n");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const namespace =
|
|
187
|
-
resolution.kind === "unknown-namespace"
|
|
188
|
-
? resolution.namespace
|
|
189
|
-
: namespaceOfKey(resolution.recipe);
|
|
190
|
-
const lines: string[] = [`in file: ${opts.fromFile}`, `namespace: ${namespace}`];
|
|
191
|
-
|
|
192
|
-
if (resolution.kind === "unknown-namespace") {
|
|
193
|
-
lines.push(`There is no recipe namespace named "${namespace}" available here.`);
|
|
194
|
-
lines.push(
|
|
195
|
-
resolution.known.length > 0
|
|
196
|
-
? `Available namespaces: ${resolution.known.join(", ")}.`
|
|
197
|
-
: "This project has no recipe namespaces available yet."
|
|
198
|
-
);
|
|
199
|
-
if (resolution.dropped) lines.push(...describeDropped(resolution.dropped, resolution.recipe));
|
|
200
|
-
} else if (resolution.kind === "unknown-recipe") {
|
|
201
|
-
lines.push(`recipe: ${resolution.recipe}`);
|
|
202
|
-
lines.push(`The namespace "${namespace}" holds no recipe named "${resolution.recipe}".`);
|
|
203
|
-
lines.push(
|
|
204
|
-
resolution.known.length > 0
|
|
205
|
-
? `Recipes in this namespace: ${resolution.known.join(", ")}.`
|
|
206
|
-
: `The namespace "${namespace}" currently holds no recipes.`
|
|
207
|
-
);
|
|
208
|
-
if (resolution.dropped) lines.push(...describeDropped(resolution.dropped, resolution.recipe));
|
|
209
|
-
} else if (resolution.kind === "not-a-dependency") {
|
|
210
|
-
lines.push(`recipe: ${resolution.recipe}`);
|
|
211
|
-
if (resolution.includingRecipe) {
|
|
212
|
-
lines.push(
|
|
213
|
-
`The recipe "${resolution.includingRecipe}" does not declare "${resolution.recipe}" as a dependency.`
|
|
214
|
-
);
|
|
215
|
-
lines.push(
|
|
216
|
-
`Add "${resolution.recipe}" to the "depends" list in that recipe's manifest before addressing it as "~${namespace}".`
|
|
217
|
-
);
|
|
218
|
-
} else {
|
|
219
|
-
lines.push(`The project's own templates may not address "${resolution.recipe}".`);
|
|
220
|
-
}
|
|
221
|
-
} else if (resolution.kind === "escapes-recipe") {
|
|
222
|
-
lines.push(`recipe: ${resolution.recipe}`);
|
|
223
|
-
lines.push(
|
|
224
|
-
`The reference "~${resolution.reference}" points outside the recipe "${resolution.recipe}".`
|
|
225
|
-
);
|
|
226
|
-
lines.push(
|
|
227
|
-
`A "~namespace" reference addresses a recipe's own files, so it may not contain ` +
|
|
228
|
-
`"." or ".." segments (also inside a { } alternative), may not be an absolute path, ` +
|
|
229
|
-
`and may not reach a file through a link that leaves the recipe.`
|
|
230
|
-
);
|
|
231
|
-
lines.push(
|
|
232
|
-
`Write the path of a file inside the recipe, or include the other file by a ` +
|
|
233
|
-
`relative path or a declared alias.`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return lines.map((line) => ` ${line}`).join("\n");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The lines that say why a recipe a project template asked for is no longer
|
|
242
|
-
* pinned, and what brings it back.
|
|
243
|
-
*
|
|
244
|
-
* @param dropped - What is known about how the recipe used to be pinned.
|
|
245
|
-
* @param recipe - The `namespace/recipe` the template asked for.
|
|
246
|
-
*/
|
|
247
|
-
function describeDropped(dropped: DroppedRecipe, recipe: string): string[] {
|
|
248
|
-
if (dropped.by === "disabled-subscription") {
|
|
249
|
-
return [
|
|
250
|
-
`This project subscribes to "${dropped.subscription}", but that subscription is ` +
|
|
251
|
-
`switched off ("enabled: false"), so the lockfile does not pin "${recipe}".`,
|
|
252
|
-
`Switch the subscription back on, or remove the include.`,
|
|
253
|
-
];
|
|
254
|
-
}
|
|
255
|
-
const through =
|
|
256
|
-
dropped.through === undefined || dropped.through.length === 0
|
|
257
|
-
? ""
|
|
258
|
-
: ` (through ${dropped.through.map((key) => `"${key}"`).join(", ")})`;
|
|
259
|
-
return [
|
|
260
|
-
`Version ${dropped.declaredAt} of "${dropped.recipe}" brought "${recipe}" in${through}, but ` +
|
|
261
|
-
`the version this project pins, ${dropped.pinned}, does not, so the lockfile no longer ` +
|
|
262
|
-
`pins it.`,
|
|
263
|
-
`Subscribe to "${recipe}" directly to keep including it, or remove the include.`,
|
|
264
|
-
];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Options for {@link StaticNamespaceResolver}. */
|
|
268
|
-
export type StaticNamespaceResolverOptions = {
|
|
269
|
-
/**
|
|
270
|
-
* Every known recipe, mapping the fully qualified ref `<namespace>/<recipe>`
|
|
271
|
-
* to the absolute directory holding that recipe's files at its pinned
|
|
272
|
-
* version. These directories double as the recipe roots used to decide which
|
|
273
|
-
* recipe an including file belongs to.
|
|
274
|
-
*/
|
|
275
|
-
recipes: Record<string, string>;
|
|
276
|
-
/**
|
|
277
|
-
* What each recipe declares, mapping `<namespace>/<recipe>` to the keys it
|
|
278
|
-
* may address. An entry is a recipe key (`workflow/task-files`) or a bare
|
|
279
|
-
* namespace (`workflow`, meaning every recipe in it), already settled from
|
|
280
|
-
* however the manifest wrote it. A recipe with no entry declares nothing and
|
|
281
|
-
* may address only itself.
|
|
282
|
-
*/
|
|
283
|
-
dependencies?: Record<string, string[]>;
|
|
284
|
-
/**
|
|
285
|
-
* What the project's own templates may address, as the same kind of keys as
|
|
286
|
-
* `dependencies`. Omit it to make every known recipe addressable from
|
|
287
|
-
* project templates, which is the rule for a real project: its templates may
|
|
288
|
-
* address everything its lockfile pins.
|
|
289
|
-
*/
|
|
290
|
-
projectScope?: string[];
|
|
291
|
-
/**
|
|
292
|
-
* Says why a recipe a project template asked for is not among `recipes`,
|
|
293
|
-
* when that can be known. Consulted only for a project template's reference
|
|
294
|
-
* to a recipe the resolver does not know.
|
|
295
|
-
*/
|
|
296
|
-
explainMissing?: (recipe: string) => DroppedRecipe | undefined;
|
|
297
|
-
/**
|
|
298
|
-
* The short name of the repository each recipe came from, keyed like
|
|
299
|
-
* `recipes`. It is what a `repo:` qualifier in an include line is matched
|
|
300
|
-
* against; a recipe with no entry matches no qualifier.
|
|
301
|
-
*/
|
|
302
|
-
repos?: Record<string, string>;
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* A dependency-free, in-memory {@link NamespaceResolver} built from a map of
|
|
307
|
-
* recipe refs to directories.
|
|
308
|
-
*
|
|
309
|
-
* It implements the full scoping rule (recipe files see their declared
|
|
310
|
-
* dependencies; project files see every known recipe, or `projectScope`) without knowing
|
|
311
|
-
* anything about repositories, versions or the store, which makes it the
|
|
312
|
-
* resolver used by tests and a usable core for the real implementation to wrap.
|
|
313
|
-
*/
|
|
314
|
-
export class StaticNamespaceResolver implements NamespaceResolver {
|
|
315
|
-
private readonly recipes: Record<string, string>;
|
|
316
|
-
private readonly dependencies: Record<string, string[]>;
|
|
317
|
-
private readonly projectScope?: string[];
|
|
318
|
-
private readonly explainMissing?: (recipe: string) => DroppedRecipe | undefined;
|
|
319
|
-
private readonly lookup: LockedRecipeFileLookup;
|
|
320
|
-
|
|
321
|
-
constructor(options: StaticNamespaceResolverOptions) {
|
|
322
|
-
this.recipes = {};
|
|
323
|
-
for (const [ref, dir] of Object.entries(options.recipes)) {
|
|
324
|
-
this.recipes[ref] = path.resolve(dir);
|
|
325
|
-
}
|
|
326
|
-
this.dependencies = options.dependencies ?? {};
|
|
327
|
-
this.projectScope = options.projectScope;
|
|
328
|
-
this.explainMissing = options.explainMissing;
|
|
329
|
-
this.lookup = new LockedRecipeFileLookup(
|
|
330
|
-
Object.keys(this.recipes).map((key) => {
|
|
331
|
-
const recipe = splitRecipeKey(key);
|
|
332
|
-
const repo = options.repos?.[key];
|
|
333
|
-
return {
|
|
334
|
-
namespace: recipe.namespace,
|
|
335
|
-
name: recipe.name ?? "",
|
|
336
|
-
...(repo === undefined ? {} : { repo }),
|
|
337
|
-
};
|
|
338
|
-
})
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Why a project template's reference names a recipe this resolver does not
|
|
344
|
-
* know, or undefined when the including file is a recipe's own or nothing is
|
|
345
|
-
* known.
|
|
346
|
-
*/
|
|
347
|
-
private droppedFor(recipe: string, fromFile: string): { dropped?: DroppedRecipe } {
|
|
348
|
-
if (this.explainMissing === undefined || this.includingRecipe(fromFile) !== null) return {};
|
|
349
|
-
const dropped = this.explainMissing(recipe);
|
|
350
|
-
return dropped === undefined ? {} : { dropped };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* The ref of the recipe whose directory contains `fromFile`, or null when the
|
|
355
|
-
* file lives outside every recipe (a project template). The deepest matching
|
|
356
|
-
* recipe directory wins, so nested layouts resolve to the innermost recipe.
|
|
357
|
-
*/
|
|
358
|
-
includingRecipe(fromFile: string): string | null {
|
|
359
|
-
const target = path.resolve(fromFile);
|
|
360
|
-
let best: string | null = null;
|
|
361
|
-
let bestLength = -1;
|
|
362
|
-
|
|
363
|
-
for (const [ref, dir] of Object.entries(this.recipes)) {
|
|
364
|
-
if (!isInside(dir, target)) continue;
|
|
365
|
-
if (dir.length > bestLength) {
|
|
366
|
-
best = ref;
|
|
367
|
-
bestLength = dir.length;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return best;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* The answer for a reference the ref service refused: a path that climbs out
|
|
376
|
-
* of the recipe is `escapes-recipe`, and anything else is `invalid`, with the
|
|
377
|
-
* service's own sentence.
|
|
378
|
-
*/
|
|
379
|
-
private refused(reference: string, error: unknown): NamespaceResolution {
|
|
380
|
-
const resolver = sharedRefResolver();
|
|
381
|
-
try {
|
|
382
|
-
const { dropped } = resolver.inspect(reference, RefSource.Include);
|
|
383
|
-
for (const entry of dropped) {
|
|
384
|
-
if (entry.ref.kind !== "recipeFile") continue;
|
|
385
|
-
if (splitSegments(entry.ref.path).some((part) => part === "." || part === "..")) {
|
|
386
|
-
return {
|
|
387
|
-
kind: "escapes-recipe",
|
|
388
|
-
recipe: `${entry.ref.recipe.namespace?.name ?? ""}/${entry.ref.recipe.name}`,
|
|
389
|
-
reference,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
394
|
-
// Not a ref in any reading; the original refusal says why.
|
|
395
|
-
}
|
|
396
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
397
|
-
return { kind: "invalid", reference, message };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
resolve(request: NamespaceRequest): NamespaceResolution {
|
|
401
|
-
const { reference, fromFile } = request;
|
|
402
|
-
const resolver = sharedRefResolver();
|
|
403
|
-
|
|
404
|
-
let result;
|
|
405
|
-
try {
|
|
406
|
-
result = resolver.resolveSync(new RefResolveArguments({
|
|
407
|
-
input: reference,
|
|
408
|
-
from: RefSource.Include,
|
|
409
|
-
lookup: this.lookup,
|
|
410
|
-
kinds: ["recipeFile"],
|
|
411
|
-
}));
|
|
412
|
-
} catch (error) {
|
|
413
|
-
return this.refused(reference, error);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// The reading the caller meant: of a `?name=value` that could also be part
|
|
417
|
-
// of a glob, the one that read it as values comes first.
|
|
418
|
-
const { kept } = resolver.inspect(reference, RefSource.Include);
|
|
419
|
-
const written = (kept.find((ref) => ref.vars !== undefined) ?? kept[0]) as RecipeFileRef;
|
|
420
|
-
const namespaceName = written.recipe.namespace?.name ?? "";
|
|
421
|
-
const wanted = `${namespaceName}/${written.recipe.name}`;
|
|
422
|
-
|
|
423
|
-
const matched = result.refs.filter((ref): ref is RecipeFileRef => ref.kind === "recipeFile");
|
|
424
|
-
if (matched.length === 0) {
|
|
425
|
-
if (written.glob === true) return { kind: "candidates", candidates: [], glob: true };
|
|
426
|
-
if (!this.lookup.knowsNamespace(namespaceName)) {
|
|
427
|
-
return {
|
|
428
|
-
kind: "unknown-namespace",
|
|
429
|
-
namespace: namespaceName,
|
|
430
|
-
recipe: wanted,
|
|
431
|
-
known: this.lookup.namespaces(),
|
|
432
|
-
...this.droppedFor(wanted, fromFile),
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
kind: "unknown-recipe",
|
|
437
|
-
recipe: wanted,
|
|
438
|
-
known: this.lookup.recipesInNamespace(namespaceName),
|
|
439
|
-
...this.droppedFor(wanted, fromFile),
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const includingRecipe = this.includingRecipe(fromFile);
|
|
444
|
-
const declared = includingRecipe
|
|
445
|
-
? [includingRecipe, ...(this.dependencies[includingRecipe] ?? [])]
|
|
446
|
-
: this.projectScope;
|
|
447
|
-
|
|
448
|
-
const candidates: string[] = [];
|
|
449
|
-
const roots: RecipeRoot[] = [];
|
|
450
|
-
for (const ref of matched) {
|
|
451
|
-
const key = `${ref.recipe.namespace?.name ?? ""}/${ref.recipe.name}`;
|
|
452
|
-
const recipeDir = this.recipes[key];
|
|
453
|
-
if (recipeDir === undefined) continue;
|
|
454
|
-
|
|
455
|
-
if (declared !== undefined && !declaresRef(declared, ref.recipe.namespace?.name ?? "", key)) {
|
|
456
|
-
if (written.glob === true) continue;
|
|
457
|
-
return { kind: "not-a-dependency", recipe: key, includingRecipe };
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (written.glob === true) {
|
|
461
|
-
// A brace alternative can hold a `.` or `..` segment that the ref
|
|
462
|
-
// service keeps inside one segment (`{..,x}`), so expand every
|
|
463
|
-
// alternative and refuse any that climbs.
|
|
464
|
-
const climbs = braceExpand(ref.path).some((alternative) =>
|
|
465
|
-
alternative.split("/").some((part) => part === "." || part === "..")
|
|
466
|
-
);
|
|
467
|
-
if (climbs) return { kind: "escapes-recipe", recipe: key, reference };
|
|
468
|
-
candidates.push(`${escapeGlob(recipeDir)}/${ref.path}`);
|
|
469
|
-
roots.push({ recipe: key, dir: recipeDir });
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const resolved = path.resolve(recipeDir, ref.path);
|
|
474
|
-
// A `~` reference addresses a recipe's own files. The ref service already
|
|
475
|
-
// refuses `.` and `..` segments; this catches whatever else would leave
|
|
476
|
-
// the recipe directory, so a reference can never have the compiler render
|
|
477
|
-
// anything on the machine into the project's output.
|
|
478
|
-
if (escapesRecipe(recipeDir, resolved)) {
|
|
479
|
-
return { kind: "escapes-recipe", recipe: key, reference };
|
|
480
|
-
}
|
|
481
|
-
candidates.push(resolved);
|
|
482
|
-
roots.push({ recipe: key, dir: recipeDir });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return written.glob === true
|
|
486
|
-
? { kind: "candidates", candidates, glob: true, roots }
|
|
487
|
-
: { kind: "candidates", candidates, roots };
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Whether a resolved path sits outside the recipe directory.
|
|
493
|
-
*
|
|
494
|
-
* @param recipeDir - The recipe's absolute directory.
|
|
495
|
-
* @param resolved - What the inner path resolved to.
|
|
496
|
-
*/
|
|
497
|
-
function escapesRecipe(recipeDir: string, resolved: string): boolean {
|
|
498
|
-
if (resolved === recipeDir) return false;
|
|
499
|
-
const relative = path.relative(recipeDir, resolved);
|
|
500
|
-
return relative === "" || relative.startsWith("..") || path.isAbsolute(relative);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Whether a list of declared keys covers a recipe, either by naming the recipe
|
|
505
|
-
* itself or by naming its whole namespace. The keys are already settled
|
|
506
|
-
* (`namespace` or `namespace/recipe`): whatever form a manifest wrote a
|
|
507
|
-
* dependency in, the lockfile records the key it resolved to.
|
|
508
|
-
*
|
|
509
|
-
* @param declared - Declared keys (dependencies, or the project's subscriptions).
|
|
510
|
-
* @param namespace - The namespace being addressed.
|
|
511
|
-
* @param ref - The fully qualified recipe ref being addressed.
|
|
512
|
-
* @returns True when the reference is in scope.
|
|
513
|
-
*/
|
|
514
|
-
function declaresRef(declared: string[], namespace: string, ref: string): boolean {
|
|
515
|
-
return declared.some((entry) => entry === ref || entry === namespace);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Whether `target` is the directory `dir` itself or sits underneath it.
|
|
520
|
-
*
|
|
521
|
-
* @param dir - An absolute directory path.
|
|
522
|
-
* @param target - An absolute file or directory path.
|
|
523
|
-
* @returns True when target is inside dir.
|
|
524
|
-
*/
|
|
525
|
-
function isInside(dir: string, target: string): boolean {
|
|
526
|
-
return target === dir || target.startsWith(dir + path.sep);
|
|
527
|
-
}
|