@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
|
@@ -1,51 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { globSync } from "glob";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
5
|
+
import { compareBytewise, hasGlob, type ViewFile } from "../services/ref-resolver/index.js";
|
|
2
6
|
import { expandHome } from "./config-discovery.js";
|
|
3
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
NamespaceResolution,
|
|
9
|
+
NamespaceResolver,
|
|
10
|
+
RecipeRoot,
|
|
11
|
+
} from "./repos/namespace-resolver.js";
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
|
-
* @include path resolution: aliases,
|
|
7
|
-
* and the ordered candidate search.
|
|
14
|
+
* @include path resolution: aliases, `#` names, recipe references, globs,
|
|
15
|
+
* variable substitution and the ordered candidate search.
|
|
8
16
|
*
|
|
9
|
-
* An include path (the part after `@`)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
17
|
+
* An include path (the part after `@`) resolves to an ordered list of groups.
|
|
18
|
+
* The caller uses the first group that names at least one file, and gets every
|
|
19
|
+
* file in it; if none does, it errors listing every path tried.
|
|
12
20
|
*
|
|
13
21
|
* Resolution pipeline for a raw path P (with leading `@` already stripped):
|
|
14
22
|
* 1. Substitute ${vars} in P, then expand a leading `~/` to the home
|
|
15
|
-
* directory (the `~` sigil
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `@${sousRootPath}/x.md`).
|
|
23
|
+
* directory (the `~` sigil with nothing but a separator after it names no
|
|
24
|
+
* namespace, so `@~/notes/x.md` is unambiguous). If the result is
|
|
25
|
+
* absolute, it is the sole candidate (`@${sousRootPath}/x.md`).
|
|
19
26
|
* 2. Split the first segment (up to the first `/` or `:`) as the alias key,
|
|
20
27
|
* the remainder as `rest`. If the key is a registered alias, push
|
|
21
|
-
* join(base, rest) for EACH base in the alias's ordered array.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
28
|
+
* join(base, rest) for EACH base in the alias's ordered array. A `#name`
|
|
29
|
+
* (`#project`) is registered in the alias map by the `#` name registry.
|
|
30
|
+
* 3. If P begins with `~` and holds a `/`, and a namespace resolver was
|
|
31
|
+
* supplied, hand the reference (P without the `~`) to it. The resolver
|
|
32
|
+
* reads it with the ref resolver service: `namespace/recipe/path`, any of
|
|
33
|
+
* the three possibly a glob, optionally with a `repo:` qualifier.
|
|
26
34
|
* 4. Always push the relative candidate: join(baseDir, P), the FULL path
|
|
27
|
-
* including the
|
|
35
|
+
* including the first segment. This lets an alias augment a real relative
|
|
28
36
|
* directory of the same name (e.g. `@stuff/x` tries the alias bases, then
|
|
29
37
|
* `./stuff/x`).
|
|
30
|
-
* 5. Follow every candidate with its `.tpl.` twin: `x.md` is
|
|
31
|
-
* `x.tpl.md`, and `x.tpl.md` by `x.md`. The literal spelling
|
|
32
|
-
* tried first, and the twin comes right after it
|
|
33
|
-
*
|
|
34
|
-
* writer therefore never has to know whether an included file has been
|
|
35
|
-
* turned into a template or back.
|
|
38
|
+
* 5. Follow every NON-glob candidate with its `.tpl.` twin: `x.md` is
|
|
39
|
+
* followed by `x.tpl.md`, and `x.tpl.md` by `x.md`. The literal spelling
|
|
40
|
+
* is always tried first, and the twin comes right after it, so an alias
|
|
41
|
+
* base still beats the relative fallback.
|
|
36
42
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
43
|
+
* Any path may use glob syntax (`*`, `**`, `?`, `[..]`, `{a,b}`) after
|
|
44
|
+
* substitution. A glob names every file it matches, in bytewise path order, and
|
|
45
|
+
* a `~` glob also matches recipes (`@~*` + `/*` + `/memories/*.md`).
|
|
40
46
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* A key WITHOUT a sigil never reaches the namespace resolver: a bare `@path` is
|
|
48
|
+
* always a relative path or a declared alias, so include lines never masquerade
|
|
49
|
+
* as filesystem paths.
|
|
50
|
+
*
|
|
51
|
+
* Aliases whose names begin with `~` or `#` are reserved: `~` for the home
|
|
52
|
+
* directory and recipe namespaces, `#` for names sous or a plugin registers.
|
|
53
|
+
* The primary separator is `/` (`@alias/path`); `:` is accepted as an
|
|
54
|
+
* equivalent for an alias (`@alias:path`).
|
|
44
55
|
*/
|
|
45
56
|
|
|
46
57
|
/** An alias maps a name to an ordered list of absolute base directories. */
|
|
47
58
|
export type AliasMap = Record<string, string[]>;
|
|
48
59
|
|
|
60
|
+
/** The files each view lists (`#memories` and so on), keyed by the name with its `#`. */
|
|
61
|
+
export type ViewMap = Record<string, ViewFile[]>;
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Substitute ${varName} references in a string from a scope. Unknown
|
|
51
65
|
* references are left untouched (matches settings.substituteVars behavior).
|
|
@@ -104,10 +118,16 @@ export function splitAliasKey(p: string): { key: string; rest: string } {
|
|
|
104
118
|
return { key: m[1], rest: m[2] };
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
/** Options shared by {@link resolveInclude} and {@link
|
|
121
|
+
/** Options shared by {@link resolveInclude} and {@link resolveIncludeFiles}. */
|
|
108
122
|
export type IncludeResolveOptions = {
|
|
109
|
-
/** The resolved alias map (name → ordered base dirs). */
|
|
123
|
+
/** The resolved alias map (name → ordered base dirs); `#` names are in it too. */
|
|
110
124
|
aliases?: AliasMap;
|
|
125
|
+
/**
|
|
126
|
+
* The files each view lists. An include of `#name/<path or glob>` for a name
|
|
127
|
+
* here selects among those virtual paths and names the real files behind the
|
|
128
|
+
* ones that match, in the listed order.
|
|
129
|
+
*/
|
|
130
|
+
views?: ViewMap;
|
|
111
131
|
/** Variable scope for ${var} substitution. */
|
|
112
132
|
scope?: Record<string, string>;
|
|
113
133
|
/** Directory of the including file (for the relative candidate). */
|
|
@@ -122,80 +142,297 @@ export type IncludeResolveOptions = {
|
|
|
122
142
|
fromFile?: string;
|
|
123
143
|
};
|
|
124
144
|
|
|
125
|
-
/** A
|
|
145
|
+
/** A `~` reference that produced no usable candidates, kept for error reporting. */
|
|
126
146
|
export type NamespaceIssue = {
|
|
127
|
-
/** The
|
|
128
|
-
|
|
129
|
-
/** The remainder of the reference (recipe name plus the path inside it). */
|
|
130
|
-
rest: string;
|
|
147
|
+
/** The reference with its `~` sigil taken off. */
|
|
148
|
+
reference: string;
|
|
131
149
|
/** The file (or directory) that performed the include. */
|
|
132
150
|
fromFile: string;
|
|
133
151
|
/** What the resolver returned. */
|
|
134
152
|
resolution: NamespaceResolution;
|
|
135
153
|
};
|
|
136
154
|
|
|
155
|
+
/**
|
|
156
|
+
* One place an include may be found. A group is found when any of its paths
|
|
157
|
+
* names at least one file, and then it supplies ALL of them: a plain include
|
|
158
|
+
* has one path per group, a glob over recipes has one per recipe.
|
|
159
|
+
*/
|
|
160
|
+
export type IncludeGroup = {
|
|
161
|
+
/** Absolute paths, or glob patterns when `glob` is true. */
|
|
162
|
+
paths: string[];
|
|
163
|
+
/** True when the paths are patterns to expand. */
|
|
164
|
+
glob: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* The recipe each path belongs to (same length and order as `paths`), when
|
|
167
|
+
* they came from a `~` reference. Every file a path names must have a real
|
|
168
|
+
* path inside that recipe's real directory; one that does not (a link
|
|
169
|
+
* leading out) is refused.
|
|
170
|
+
*/
|
|
171
|
+
roots?: RecipeRoot[];
|
|
172
|
+
};
|
|
173
|
+
|
|
137
174
|
/** The full result of resolving one include path. */
|
|
138
175
|
export type IncludeResolution = {
|
|
139
|
-
/**
|
|
176
|
+
/** Every group, in the order they are tried; the first that names a file wins. */
|
|
177
|
+
groups: IncludeGroup[];
|
|
178
|
+
/** Every path or pattern, flattened, in order and de-duplicated: what an error lists as tried. */
|
|
140
179
|
candidates: string[];
|
|
180
|
+
/** True when the include path holds glob syntax. */
|
|
181
|
+
glob: boolean;
|
|
141
182
|
/**
|
|
142
|
-
* Present when
|
|
143
|
-
*
|
|
144
|
-
*
|
|
183
|
+
* Present when a `~` reference could not be satisfied. The groups are still
|
|
184
|
+
* usable (they hold the alias and relative fallbacks); this only explains
|
|
185
|
+
* what went wrong with the reference.
|
|
145
186
|
*/
|
|
146
187
|
namespaceIssue?: NamespaceIssue;
|
|
188
|
+
/** Present when the path starts with a `#` name nothing registered. */
|
|
189
|
+
hashIssue?: string;
|
|
190
|
+
/** True when the path names a view; a glob over a view may match nothing without being an error. */
|
|
191
|
+
view?: boolean;
|
|
147
192
|
};
|
|
148
193
|
|
|
194
|
+
/** The files an include names, and how the search went. */
|
|
195
|
+
export type IncludeFiles = {
|
|
196
|
+
/** Every file the include names, each once, in the order they are included. */
|
|
197
|
+
files: string[];
|
|
198
|
+
/** Everything that was tried. */
|
|
199
|
+
candidates: string[];
|
|
200
|
+
/** True when the include path holds glob syntax. */
|
|
201
|
+
glob: boolean;
|
|
202
|
+
/** Why a `~` reference failed, when it did. */
|
|
203
|
+
namespaceIssue?: NamespaceIssue;
|
|
204
|
+
/** Why a `#` name failed, when it did. */
|
|
205
|
+
hashIssue?: string;
|
|
206
|
+
/** True when the path names a view. */
|
|
207
|
+
view?: boolean;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** The `?name=value&...` query an include line may end with, after its `.md`. */
|
|
211
|
+
const QUERY_SUFFIX = /^(.*\.md)(\?[^?\s]*=\S*)$/;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Splits the query off an include path: `a/b.md?name=value` gives the path
|
|
215
|
+
* `a/b.md` and the query `?name=value`.
|
|
216
|
+
*
|
|
217
|
+
* @param raw - The include path, without its `@`.
|
|
218
|
+
*/
|
|
219
|
+
export function splitIncludeQuery(raw: string): { path: string; query: string } {
|
|
220
|
+
const match = QUERY_SUFFIX.exec(raw);
|
|
221
|
+
return match === null ? { path: raw, query: "" } : { path: match[1]!, query: match[2]! };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The group of one literal path followed by its `.tpl.` twin. */
|
|
225
|
+
function literalGroups(candidate: string, root?: RecipeRoot): IncludeGroup[] {
|
|
226
|
+
return withTemplateTwins([candidate]).map((entry) => ({
|
|
227
|
+
paths: [entry],
|
|
228
|
+
glob: false,
|
|
229
|
+
...(root === undefined ? {} : { roots: [root] }),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Whether a file's real path sits inside a directory's real path, so a link
|
|
235
|
+
* cannot lead out of it. A path that cannot be resolved (it does not exist) is
|
|
236
|
+
* not an escape.
|
|
237
|
+
*
|
|
238
|
+
* @param file - An absolute file path.
|
|
239
|
+
* @param dir - An absolute directory path.
|
|
240
|
+
*/
|
|
241
|
+
export function realPathInside(file: string, dir: string): boolean {
|
|
242
|
+
let realFile: string;
|
|
243
|
+
let realDir: string;
|
|
244
|
+
try {
|
|
245
|
+
realFile = fs.realpathSync(file);
|
|
246
|
+
realDir = fs.realpathSync(dir);
|
|
247
|
+
} catch {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return realFile === realDir || realFile.startsWith(realDir + path.sep);
|
|
251
|
+
}
|
|
252
|
+
|
|
149
253
|
/**
|
|
150
|
-
* Resolve an include path to its ordered
|
|
151
|
-
*
|
|
152
|
-
* reference failed; {@link
|
|
254
|
+
* Resolve an include path to its ordered groups plus any diagnostic. Nothing is
|
|
255
|
+
* read from disk. Use this when the caller wants to report WHY a `~` or `#`
|
|
256
|
+
* reference failed; {@link resolveIncludeFiles} reads the disk and returns the
|
|
257
|
+
* files.
|
|
153
258
|
*
|
|
154
259
|
* @param rawPath - The include path with the leading `@` already stripped.
|
|
155
260
|
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
156
|
-
* @returns The candidate list and, when a
|
|
261
|
+
* @returns The groups, the flat candidate list and, when a reference failed, the reason.
|
|
157
262
|
*/
|
|
158
263
|
export function resolveInclude(rawPath: string, opts: IncludeResolveOptions): IncludeResolution {
|
|
159
264
|
const aliases = opts.aliases ?? {};
|
|
160
265
|
const scope = opts.scope ?? {};
|
|
161
|
-
const
|
|
266
|
+
const withQuery = expandHome(substituteVars(rawPath, scope));
|
|
267
|
+
const substituted = splitIncludeQuery(withQuery).path;
|
|
268
|
+
const glob = hasGlob(substituted);
|
|
269
|
+
|
|
270
|
+
const finish = (
|
|
271
|
+
groups: IncludeGroup[],
|
|
272
|
+
extra: Pick<IncludeResolution, "namespaceIssue" | "hashIssue" | "view"> = {}
|
|
273
|
+
): IncludeResolution => ({
|
|
274
|
+
groups,
|
|
275
|
+
candidates: [...new Set(groups.flatMap((group) => group.paths))],
|
|
276
|
+
glob,
|
|
277
|
+
...extra,
|
|
278
|
+
});
|
|
279
|
+
const single = (candidate: string): IncludeGroup[] =>
|
|
280
|
+
glob ? [{ paths: [candidate], glob: true }] : literalGroups(candidate);
|
|
162
281
|
|
|
163
|
-
// 1. Substituted (or home-expanded) to an absolute path
|
|
282
|
+
// 1. Substituted (or home-expanded) to an absolute path: that, and its twin.
|
|
164
283
|
if (path.isAbsolute(substituted)) {
|
|
165
|
-
return
|
|
284
|
+
return finish(single(path.normalize(substituted)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A view lists virtual files; the include selects among them by glob or by
|
|
288
|
+
// exact virtual path and names the real files behind the matches.
|
|
289
|
+
const views = opts.views ?? {};
|
|
290
|
+
const viewKey = splitAliasKey(substituted);
|
|
291
|
+
if (viewKey.key.startsWith("#") && Object.prototype.hasOwnProperty.call(views, viewKey.key)) {
|
|
292
|
+
const includingRecipe = opts.namespaceResolver?.includingRecipe?.(opts.fromFile ?? opts.baseDir) ?? null;
|
|
293
|
+
if (includingRecipe !== null) {
|
|
294
|
+
return finish([], {
|
|
295
|
+
view: true,
|
|
296
|
+
hashIssue:
|
|
297
|
+
`The recipe "${includingRecipe}" may not include "${viewKey.key}", because a view lists ` +
|
|
298
|
+
`other recipes' files.\n` +
|
|
299
|
+
`A recipe includes another recipe's files with "@~namespace/recipe/..." and a ` +
|
|
300
|
+
`dependency declared in its manifest ("depends" or "subscribes").`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const listed = views[viewKey.key]!;
|
|
304
|
+
const wanted = viewKey.rest;
|
|
305
|
+
const matched = listed.filter((entry) =>
|
|
306
|
+
glob ? minimatch(entry.path, wanted, { dot: true }) : entry.path === wanted
|
|
307
|
+
);
|
|
308
|
+
const issue =
|
|
309
|
+
!glob && matched.length === 0
|
|
310
|
+
? `The view "${viewKey.key}" lists no file at "${wanted}".\n` +
|
|
311
|
+
(listed.length > 0
|
|
312
|
+
? `It lists:\n${listed.map((entry) => ` ${viewKey.key}/${entry.path}`).join("\n")}`
|
|
313
|
+
: "It lists no files in this project.")
|
|
314
|
+
: undefined;
|
|
315
|
+
return finish(
|
|
316
|
+
matched.length > 0 ? [{ paths: [...new Set(matched.map((entry) => entry.file))], glob: false }] : [],
|
|
317
|
+
{ view: true, ...(issue === undefined ? {} : { hashIssue: issue }) }
|
|
318
|
+
);
|
|
166
319
|
}
|
|
167
320
|
|
|
168
|
-
const
|
|
321
|
+
const groups: IncludeGroup[] = [];
|
|
169
322
|
let namespaceIssue: NamespaceIssue | undefined;
|
|
323
|
+
let hashIssue: string | undefined;
|
|
170
324
|
|
|
171
|
-
// 2. Alias bases (ordered), if the first segment is a registered alias.
|
|
325
|
+
// 2. Alias bases (ordered), if the first segment is a registered alias. A
|
|
326
|
+
// `#` name is an alias too: the registry puts every `#name` in the map.
|
|
172
327
|
const { key, rest } = splitAliasKey(substituted);
|
|
173
328
|
if (key && Object.prototype.hasOwnProperty.call(aliases, key)) {
|
|
174
|
-
for (const base of aliases[key])
|
|
175
|
-
|
|
176
|
-
|
|
329
|
+
for (const base of aliases[key]!) groups.push(...single(path.resolve(base, rest)));
|
|
330
|
+
} else if (key.startsWith("#") && rest.length > 0) {
|
|
331
|
+
const known = [...Object.keys(aliases), ...Object.keys(views)].filter((name) =>
|
|
332
|
+
name.startsWith("#")
|
|
333
|
+
);
|
|
334
|
+
hashIssue =
|
|
335
|
+
`There is no built-in name "${key}" available here.\n` +
|
|
336
|
+
(known.length > 0
|
|
337
|
+
? `Available names: ${known.sort().join(", ")}.`
|
|
338
|
+
: "No '#' names are available in this project.");
|
|
177
339
|
}
|
|
178
340
|
|
|
179
|
-
// 3.
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
if (
|
|
183
|
-
|
|
341
|
+
// 3. A recipe reference: a `~` sigil, then namespace, recipe and path. Only
|
|
342
|
+
// when a resolver was supplied and a `/` follows, so a file that is
|
|
343
|
+
// simply named `~notes.md` is still a relative path.
|
|
344
|
+
if (
|
|
345
|
+
opts.namespaceResolver &&
|
|
346
|
+
substituted.startsWith("~") &&
|
|
347
|
+
substituted.length > 1 &&
|
|
348
|
+
substituted.includes("/")
|
|
349
|
+
) {
|
|
350
|
+
const reference = withQuery.slice(1);
|
|
184
351
|
const fromFile = opts.fromFile ?? opts.baseDir;
|
|
185
|
-
const resolution = opts.namespaceResolver.resolve({
|
|
352
|
+
const resolution = opts.namespaceResolver.resolve({ reference, fromFile });
|
|
186
353
|
|
|
187
|
-
if (resolution.kind === "candidates" && resolution.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
354
|
+
if (resolution.kind === "candidates" && resolution.glob === true) {
|
|
355
|
+
groups.push({
|
|
356
|
+
paths: resolution.candidates.map((c) => c),
|
|
357
|
+
glob: true,
|
|
358
|
+
...(resolution.roots === undefined ? {} : { roots: resolution.roots }),
|
|
359
|
+
});
|
|
360
|
+
} else if (resolution.kind === "candidates" && resolution.candidates.length > 0) {
|
|
361
|
+
resolution.candidates.forEach((candidate, index) => {
|
|
362
|
+
groups.push(...literalGroups(path.normalize(candidate), resolution.roots?.[index]));
|
|
363
|
+
});
|
|
364
|
+
} else if (resolution.kind !== "candidates") {
|
|
365
|
+
namespaceIssue = { reference, fromFile, resolution };
|
|
191
366
|
}
|
|
192
367
|
}
|
|
193
368
|
|
|
194
369
|
// 4. Relative fallback: the FULL substituted path under the including dir.
|
|
195
|
-
|
|
370
|
+
groups.push(...single(path.resolve(opts.baseDir, substituted)));
|
|
371
|
+
|
|
372
|
+
return finish(groups, {
|
|
373
|
+
...(namespaceIssue === undefined ? {} : { namespaceIssue }),
|
|
374
|
+
...(hashIssue === undefined ? {} : { hashIssue }),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
196
377
|
|
|
197
|
-
|
|
198
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Expands a glob pattern to the files it matches, sorted by path bytewise so
|
|
380
|
+
* the order is the same on every machine.
|
|
381
|
+
*
|
|
382
|
+
* @param pattern - An absolute glob pattern.
|
|
383
|
+
*/
|
|
384
|
+
function expandPattern(pattern: string): string[] {
|
|
385
|
+
return globSync(pattern, { absolute: true, nodir: true, dot: true }).sort(compareBytewise);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolve an include path to the files it names. The groups are tried in
|
|
390
|
+
* order and the first that names any file supplies all of them: a plain path
|
|
391
|
+
* is one file, a glob is every file it matches in sorted path order.
|
|
392
|
+
*
|
|
393
|
+
* @param rawPath - The include path with the leading `@` already stripped.
|
|
394
|
+
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
395
|
+
* @returns The files (empty when nothing was found) and what was tried.
|
|
396
|
+
*/
|
|
397
|
+
export function resolveIncludeFiles(rawPath: string, opts: IncludeResolveOptions): IncludeFiles {
|
|
398
|
+
const resolution = resolveInclude(rawPath, opts);
|
|
399
|
+
const out: IncludeFiles = {
|
|
400
|
+
files: [],
|
|
401
|
+
candidates: resolution.candidates,
|
|
402
|
+
glob: resolution.glob,
|
|
403
|
+
...(resolution.namespaceIssue === undefined ? {} : { namespaceIssue: resolution.namespaceIssue }),
|
|
404
|
+
...(resolution.hashIssue === undefined ? {} : { hashIssue: resolution.hashIssue }),
|
|
405
|
+
...(resolution.view === true ? { view: true } : {}),
|
|
406
|
+
};
|
|
407
|
+
for (const group of resolution.groups) {
|
|
408
|
+
const found = group.glob
|
|
409
|
+
? group.paths.flatMap(expandPattern)
|
|
410
|
+
: group.paths.filter((candidate) => fs.existsSync(candidate));
|
|
411
|
+
if (group.roots !== undefined) {
|
|
412
|
+
const roots = group.roots;
|
|
413
|
+
const leaving = group.paths.flatMap((entry, index) => {
|
|
414
|
+
const root = roots[index];
|
|
415
|
+
if (root === undefined) return [];
|
|
416
|
+
const files = group.glob ? expandPattern(entry) : fs.existsSync(entry) ? [entry] : [];
|
|
417
|
+
return files.filter((file) => !realPathInside(file, root.dir)).map(() => root);
|
|
418
|
+
});
|
|
419
|
+
const { recipe } = leaving[0] ?? { recipe: "" };
|
|
420
|
+
if (leaving.length > 0) {
|
|
421
|
+
out.files = [];
|
|
422
|
+
out.namespaceIssue = {
|
|
423
|
+
reference: rawPath.replace(/^~/, ""),
|
|
424
|
+
fromFile: opts.fromFile ?? opts.baseDir,
|
|
425
|
+
resolution: { kind: "escapes-recipe", recipe, reference: rawPath.replace(/^~/, "") },
|
|
426
|
+
};
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (found.length > 0) {
|
|
431
|
+
out.files = [...new Set(found)];
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
199
436
|
}
|
|
200
437
|
|
|
201
438
|
/**
|
|
@@ -203,7 +440,7 @@ export function resolveInclude(rawPath: string, opts: IncludeResolveOptions): In
|
|
|
203
440
|
*
|
|
204
441
|
* @param rawPath - The include path with the leading `@` already stripped.
|
|
205
442
|
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
206
|
-
* @returns Ordered, de-duplicated absolute candidate paths.
|
|
443
|
+
* @returns Ordered, de-duplicated absolute candidate paths (patterns, for a glob).
|
|
207
444
|
*/
|
|
208
445
|
export function resolveIncludeCandidates(rawPath: string, opts: IncludeResolveOptions): string[] {
|
|
209
446
|
return resolveInclude(rawPath, opts).candidates;
|
|
@@ -238,10 +475,10 @@ export function resolveAliasPrefix(p: string, aliases: AliasMap): string[] {
|
|
|
238
475
|
* Each user alias value may be a single string or an array of strings, and
|
|
239
476
|
* each is run through ${var} substitution against `scope`.
|
|
240
477
|
*
|
|
241
|
-
* User aliases may NOT use names beginning with `~` (reserved
|
|
478
|
+
* User aliases may NOT use names beginning with `~` or `#` (reserved);
|
|
242
479
|
* such entries are rejected via `onError` and ignored.
|
|
243
480
|
*
|
|
244
|
-
* @param opts.builtIns - Built-in alias map (already absolute;
|
|
481
|
+
* @param opts.builtIns - Built-in alias map (already absolute; `#`-prefixed names).
|
|
245
482
|
* @param opts.userAliases - Ordered list of user `_aliases` blocks (root, then project).
|
|
246
483
|
* @param opts.scope - Variable scope for substituting alias values.
|
|
247
484
|
* @param opts.onError - Called with a message for each rejected/invalid entry.
|
|
@@ -266,9 +503,10 @@ export function buildAliasMap(opts: {
|
|
|
266
503
|
for (const block of opts.userAliases ?? []) {
|
|
267
504
|
if (!block) continue;
|
|
268
505
|
for (const [name, value] of Object.entries(block)) {
|
|
269
|
-
if (name.startsWith("~")) {
|
|
506
|
+
if (name.startsWith("~") || name.startsWith("#")) {
|
|
270
507
|
opts.onError?.(
|
|
271
|
-
`Alias "${name}" is invalid: names beginning with "~" are reserved
|
|
508
|
+
`Alias "${name}" is invalid: names beginning with "~" or "#" are reserved ` +
|
|
509
|
+
`("~" for the home directory and recipe namespaces, "#" for built-in names).`
|
|
272
510
|
);
|
|
273
511
|
continue;
|
|
274
512
|
}
|