@sous-io/sous 0.2.2 → 0.2.4
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 +18 -9
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +21 -0
- package/docs/markdown/config-discovery.md +3 -2
- package/docs/markdown/config-inspection.md +4 -1
- package/docs/markdown/configuration.md +2 -1
- package/docs/markdown/repositories-consuming.md +3 -1
- package/docs/markdown/repositories-quickstart.md +4 -1
- package/docs/markdown/repositories-variables.md +33 -14
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/base-command.ts +76 -8
- package/src/commands/build.ts +3 -80
- package/src/commands/compile.ts +4 -2
- package/src/commands/init.ts +269 -0
- package/src/lib/build-preparation.ts +86 -0
- package/src/lib/build-service.ts +52 -4
- package/src/lib/config-discovery.ts +2 -16
- package/src/lib/markdown-compiler.ts +20 -1
- package/src/lib/project-scaffold/index.ts +251 -0
- package/src/lib/project-scaffold/templates.ts +230 -0
- package/src/lib/repos/links.ts +3 -1
- package/src/lib/repos/recipe-targets.ts +9 -1
- package/src/lib/settings.ts +51 -3
- package/src/lib/vars/answers.ts +184 -0
- package/src/lib/vars/definition-source.ts +9 -0
- package/src/lib/vars/index.ts +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The answers a build renders with.
|
|
3
|
+
*
|
|
4
|
+
* A recipe publishes variable DEFINITIONS; the project's env files and shell
|
|
5
|
+
* hold the ANSWERS; the ladder (`ladder.ts`) says which answer a definition
|
|
6
|
+
* gets. This module is the one place that turns all of that into the variable
|
|
7
|
+
* scope a template renders from, so a `{{ variable }}` in a recipe's own skill
|
|
8
|
+
* sees the answer to its own question without the project mapping the name by
|
|
9
|
+
* hand through `_env`.
|
|
10
|
+
*
|
|
11
|
+
* Three views come out of one walk over the definitions:
|
|
12
|
+
*
|
|
13
|
+
* - `merged`, what the project's own templates render: one value per
|
|
14
|
+
* variable name. Two recipes may publish the same name (the shared rung of
|
|
15
|
+
* the ladder exists for exactly that), so the first definition in lockfile
|
|
16
|
+
* order wins the merged view, and a project that wants something else says
|
|
17
|
+
* so in `_vars`, which sits above every answer.
|
|
18
|
+
* - `byRecipe`, what a recipe's own files render: the answers resolved for
|
|
19
|
+
* that recipe's definitions, which the recipe-scoped rung of the ladder can
|
|
20
|
+
* make different from the merged view.
|
|
21
|
+
* - `unanswered`, every required definition that no rung and no default
|
|
22
|
+
* answered. A build reports those and carries on, because a missing answer
|
|
23
|
+
* is something to tell the user about, not a reason to refuse to build the
|
|
24
|
+
* rest of the project.
|
|
25
|
+
*
|
|
26
|
+
* A definition's `default` counts as an answer of last resort: the
|
|
27
|
+
* description a publisher writes promises what the default does, and a
|
|
28
|
+
* template rendering an empty string instead would break that promise.
|
|
29
|
+
*
|
|
30
|
+
* Values are laid in exactly as the ladder found them. Nothing here resolves a
|
|
31
|
+
* path or coerces a number; the answer is the string the user stored, the same
|
|
32
|
+
* string an `_env` mapping would deliver.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { Settings, VarScope } from "../settings.js";
|
|
36
|
+
import { BULLET } from "../../utils/formatting.js";
|
|
37
|
+
import {
|
|
38
|
+
ProjectDefinitionSource,
|
|
39
|
+
definingRecipeKey,
|
|
40
|
+
type DefinedVariable,
|
|
41
|
+
} from "./definition-source.js";
|
|
42
|
+
import { loadLadderContext, resolveVariable, type LadderContext } from "./ladder.js";
|
|
43
|
+
|
|
44
|
+
/** The answers in play for a project, in the three views a build needs. */
|
|
45
|
+
export interface RecipeAnswers {
|
|
46
|
+
/** One value per variable name; the first definition in lockfile order wins a name. */
|
|
47
|
+
merged: VarScope;
|
|
48
|
+
/** The answers resolved for each recipe's own definitions, keyed `namespace/recipe`. */
|
|
49
|
+
byRecipe: Map<string, VarScope>;
|
|
50
|
+
/** Every required definition that nothing answered and that has no default. */
|
|
51
|
+
unanswered: DefinedVariable[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How the answers are resolved. */
|
|
55
|
+
export interface RecipeAnswerOptions {
|
|
56
|
+
/** The merged project config, read for its `varMappings` block. */
|
|
57
|
+
settings: Settings;
|
|
58
|
+
/** The project's `.sous/` directory, which holds the lockfile and both env files. */
|
|
59
|
+
sousDir: string;
|
|
60
|
+
/**
|
|
61
|
+
* The definitions to answer. Defaults to every definition the project's
|
|
62
|
+
* lockfile pins; tests hand in a fixed list.
|
|
63
|
+
*/
|
|
64
|
+
definitions?: DefinedVariable[];
|
|
65
|
+
/**
|
|
66
|
+
* The environment the ladder treats as the shell. A build passes nothing and
|
|
67
|
+
* gets `process.env`, which already holds every env-file value in precedence
|
|
68
|
+
* order (the files are loaded first-writer-wins, shell first), so the value
|
|
69
|
+
* that comes back is the right one; only the layer it is attributed to would
|
|
70
|
+
* differ, and a build does not report that.
|
|
71
|
+
*/
|
|
72
|
+
shellEnv?: NodeJS.ProcessEnv;
|
|
73
|
+
/** The environment that decides where the store is; defaults to `process.env`. */
|
|
74
|
+
env?: NodeJS.ProcessEnv;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The empty result, for a project that locks nothing. */
|
|
78
|
+
export function noRecipeAnswers(): RecipeAnswers {
|
|
79
|
+
return { merged: {}, byRecipe: new Map(), unanswered: [] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves every definition in play through the ladder and lays the answers
|
|
84
|
+
* out in the three views a build renders and reports with.
|
|
85
|
+
*
|
|
86
|
+
* @param options - The project's config, its `.sous/` directory, and optionally
|
|
87
|
+
* the definitions and environment to use instead of the real ones.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveRecipeAnswers(options: RecipeAnswerOptions): RecipeAnswers {
|
|
90
|
+
const definitions =
|
|
91
|
+
options.definitions ??
|
|
92
|
+
new ProjectDefinitionSource(options.settings, options.sousDir, options.env).loadSync();
|
|
93
|
+
if (definitions.length === 0) return noRecipeAnswers();
|
|
94
|
+
|
|
95
|
+
const context: LadderContext = loadLadderContext({
|
|
96
|
+
sousDir: options.sousDir,
|
|
97
|
+
settings: options.settings,
|
|
98
|
+
...(options.shellEnv === undefined ? {} : { shellEnv: options.shellEnv }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = noRecipeAnswers();
|
|
102
|
+
|
|
103
|
+
for (const defined of definitions) {
|
|
104
|
+
const value = answerFor(defined, context);
|
|
105
|
+
const name = defined.definition.name;
|
|
106
|
+
|
|
107
|
+
if (value === undefined) {
|
|
108
|
+
if (defined.definition.required) result.unanswered.push(defined);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!(name in result.merged)) result.merged[name] = value;
|
|
113
|
+
|
|
114
|
+
const recipeKey = definingRecipeKey(defined.recipe);
|
|
115
|
+
const own = result.byRecipe.get(recipeKey) ?? {};
|
|
116
|
+
own[name] = value;
|
|
117
|
+
result.byRecipe.set(recipeKey, own);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The answer one definition renders with: what the ladder found, else the
|
|
125
|
+
* definition's own default, else nothing.
|
|
126
|
+
*
|
|
127
|
+
* @param defined - The definition and the recipe that published it.
|
|
128
|
+
* @param context - The environment layers and mapping records.
|
|
129
|
+
*/
|
|
130
|
+
function answerFor(defined: DefinedVariable, context: LadderContext): string | undefined {
|
|
131
|
+
const resolved = resolveVariable(defined, context);
|
|
132
|
+
if (resolved !== undefined) return resolved.value;
|
|
133
|
+
const fallback = defined.definition.default;
|
|
134
|
+
return fallback === undefined ? undefined : String(fallback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The scope a recipe's own files render with: the merged view, with that
|
|
139
|
+
* recipe's own answers laid over it.
|
|
140
|
+
*
|
|
141
|
+
* @param answers - The resolved answers.
|
|
142
|
+
* @param recipeKey - The recipe, as `namespace/recipe`.
|
|
143
|
+
*/
|
|
144
|
+
export function answersForRecipe(answers: RecipeAnswers, recipeKey: string): VarScope {
|
|
145
|
+
return { ...answers.merged, ...(answers.byRecipe.get(recipeKey) ?? {}) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The one warning a build prints when required variables are unanswered: every
|
|
150
|
+
* variable named with the recipe that asks for it, and the command that answers
|
|
151
|
+
* them. Undefined when nothing is missing.
|
|
152
|
+
*
|
|
153
|
+
* A variable the project's config defines itself, in `_vars` or through
|
|
154
|
+
* `_env`, is not missing: the template renders that value, whatever the ladder
|
|
155
|
+
* found. So the check is made against the scope the templates actually render
|
|
156
|
+
* with, not against the ladder alone.
|
|
157
|
+
*
|
|
158
|
+
* @param answers - The resolved answers.
|
|
159
|
+
* @param renderScope - The scope the project's templates render with.
|
|
160
|
+
*/
|
|
161
|
+
export function unansweredWarning(
|
|
162
|
+
answers: RecipeAnswers,
|
|
163
|
+
renderScope: VarScope = {}
|
|
164
|
+
): string | undefined {
|
|
165
|
+
const missing = answers.unanswered.filter(
|
|
166
|
+
(defined) => renderScope[defined.definition.name] === undefined
|
|
167
|
+
);
|
|
168
|
+
if (missing.length === 0) return undefined;
|
|
169
|
+
|
|
170
|
+
const lines = missing.map(
|
|
171
|
+
(defined) =>
|
|
172
|
+
` ${BULLET} ${defined.definition.name} (asked by ${definingRecipeKey(defined.recipe)})`
|
|
173
|
+
);
|
|
174
|
+
const count = missing.length;
|
|
175
|
+
const noun = count === 1 ? "variable" : "variables";
|
|
176
|
+
const pronoun = count === 1 ? "it" : "them";
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
`${count} required recipe ${noun} ${count === 1 ? "has" : "have"} no answer, so the ` +
|
|
180
|
+
`templates that use ${pronoun} render an empty value:\n` +
|
|
181
|
+
`${lines.join("\n")}\n` +
|
|
182
|
+
`Run 'sous vars ask' to answer ${pronoun}.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -111,6 +111,15 @@ export class ProjectDefinitionSource implements VariableDefinitionSource {
|
|
|
111
111
|
|
|
112
112
|
/** Every variable published by every recipe this project's lockfile pins. */
|
|
113
113
|
async load(): Promise<DefinedVariable[]> {
|
|
114
|
+
return this.loadSync();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The same list, read synchronously. The lockfile and the manifests are
|
|
119
|
+
* ordinary files, and the settings scope a build renders with is assembled
|
|
120
|
+
* synchronously, so the build path needs this form.
|
|
121
|
+
*/
|
|
122
|
+
loadSync(): DefinedVariable[] {
|
|
114
123
|
void this.settings;
|
|
115
124
|
|
|
116
125
|
const defined: DefinedVariable[] = [];
|