rman 1.3.0 → 2.0.0-beta.3
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 +65 -31
- package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
- package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
- package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
- package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
- package/builtins/platforms/node/commands/ci.command.js +60 -0
- package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
- package/builtins/platforms/node/commands/clean.command.js +74 -0
- package/builtins/platforms/node/index.d.ts +8 -0
- package/builtins/platforms/node/index.js +8 -0
- package/builtins/platforms/node/node-config.interface.d.ts +76 -0
- package/builtins/platforms/node/node-config.interface.js +7 -0
- package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
- package/builtins/platforms/node/node-manifest.provider.js +125 -0
- package/builtins/platforms/node/node.platform.d.ts +53 -0
- package/builtins/platforms/node/node.platform.js +134 -0
- package/builtins/platforms/node/services/ci.service.d.ts +47 -0
- package/builtins/platforms/node/services/ci.service.js +213 -0
- package/builtins/platforms/node/services/clean.service.d.ts +53 -0
- package/builtins/platforms/node/services/clean.service.js +237 -0
- package/builtins/platforms/node/services/publish.service.d.ts +114 -0
- package/builtins/platforms/node/services/publish.service.js +371 -0
- package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
- package/builtins/platforms/node/services/version-plan.service.js +58 -0
- package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
- package/builtins/platforms/node/utils/workspace-range.js +28 -0
- package/builtins/presets/index.d.ts +15 -0
- package/builtins/presets/index.js +57 -0
- package/builtins/presets/node.d.ts +16 -0
- package/builtins/presets/node.js +57 -0
- package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
- package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
- package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
- package/builtins/publish-targets/docker/docker.target.js +40 -0
- package/builtins/publish-targets/docker/index.d.ts +2 -0
- package/builtins/publish-targets/docker/index.js +2 -0
- package/builtins/publish-targets/npm/index.d.ts +2 -0
- package/builtins/publish-targets/npm/index.js +2 -0
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
- package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
- package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
- package/builtins/publish-targets/npm/npm-view.js +71 -0
- package/cli.d.ts +8 -0
- package/cli.js +228 -93
- package/commands/build.command.d.ts +177 -3
- package/commands/build.command.js +20 -10
- package/commands/changelog.command.d.ts +296 -3
- package/commands/changelog.command.js +127 -43
- package/commands/config.command.d.ts +44 -3
- package/commands/config.command.js +53 -24
- package/commands/diff.command.d.ts +37 -3
- package/commands/diff.command.js +25 -16
- package/commands/exec.command.d.ts +193 -3
- package/commands/exec.command.js +60 -58
- package/commands/github-release.command.d.ts +154 -3
- package/commands/github-release.command.js +67 -37
- package/commands/import.command.d.ts +36 -3
- package/commands/import.command.js +28 -20
- package/commands/info.command.d.ts +35 -7
- package/commands/info.command.js +36 -30
- package/commands/lint.command.d.ts +176 -0
- package/commands/lint.command.js +34 -0
- package/commands/list.command.d.ts +163 -3
- package/commands/list.command.js +109 -71
- package/commands/publish.command.d.ts +231 -0
- package/commands/publish.command.js +304 -0
- package/commands/run.command.d.ts +186 -6
- package/commands/run.command.js +26 -72
- package/commands/test.command.d.ts +173 -3
- package/commands/test.command.js +16 -10
- package/commands/version.command.d.ts +330 -3
- package/commands/version.command.js +292 -78
- package/commands.d.ts +32 -0
- package/commands.js +28 -0
- package/constants.js +1 -1
- package/core/application.d.ts +116 -0
- package/core/application.js +160 -0
- package/core/classes/package.d.ts +219 -0
- package/core/classes/package.js +214 -0
- package/core/classes/registry.d.ts +30 -0
- package/core/classes/registry.js +47 -0
- package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
- package/core/{repository.js → classes/repository.js} +195 -75
- package/core/classes/service.d.ts +49 -0
- package/core/classes/service.js +40 -0
- package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
- package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
- package/core/classes/workspace.d.ts +275 -0
- package/core/classes/workspace.js +537 -0
- package/core/command-builder.d.ts +14 -0
- package/core/command-builder.js +94 -0
- package/core/config/config-file-scope.d.ts +107 -0
- package/core/config/config-file-scope.js +226 -0
- package/core/config/config-interpolator.d.ts +164 -0
- package/core/config/config-interpolator.js +577 -0
- package/core/config/config-paths.d.ts +65 -0
- package/core/config/config-paths.js +78 -0
- package/core/config/config-reader.d.ts +192 -0
- package/core/config/config-reader.js +727 -0
- package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
- package/core/config/merge-config.js +260 -0
- package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
- package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
- package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
- package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
- package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
- package/core/interfaces/manifest.js +132 -0
- package/core/interfaces/plugin.d.ts +174 -0
- package/core/interfaces/plugin.js +124 -0
- package/core/interfaces/publish-target.d.ts +136 -0
- package/core/interfaces/publish-target.js +43 -0
- package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
- package/core/interfaces/run-step.js +1 -0
- package/core/plugin-loader.d.ts +25 -0
- package/core/plugin-loader.js +53 -0
- package/index.d.ts +122 -24
- package/index.js +66 -16
- package/interfaces/config-scope.interface.d.ts +223 -0
- package/interfaces/config-scope.interface.js +1 -0
- package/interfaces/rman-config.interface.d.ts +821 -203
- package/interfaces/rman-config.interface.js +78 -1
- package/package.json +3 -1
- package/services/change-hash.service.d.ts +42 -8
- package/services/change-hash.service.js +92 -13
- package/services/changelog.service.d.ts +91 -52
- package/services/changelog.service.js +460 -74
- package/services/config-service.d.ts +8 -0
- package/services/config-service.js +3 -0
- package/services/exec.service.d.ts +23 -12
- package/services/exec.service.js +14 -9
- package/services/github-release.service.d.ts +45 -34
- package/services/github-release.service.js +15 -12
- package/services/import.service.d.ts +25 -14
- package/services/import.service.js +9 -5
- package/services/list.service.d.ts +67 -13
- package/services/list.service.js +62 -15
- package/services/run.service.d.ts +35 -22
- package/services/run.service.js +316 -233
- package/services/system-info.d.ts +1 -1
- package/services/version-plan.service.d.ts +35 -8
- package/services/version-plan.service.js +96 -36
- package/services/version.service.d.ts +34 -14
- package/services/version.service.js +35 -16
- package/services.d.ts +1 -1
- package/services.js +1 -1
- package/utils/bin-path.d.ts +6 -7
- package/utils/bin-path.js +7 -18
- package/utils/branch-guard.d.ts +30 -1
- package/utils/branch-guard.js +31 -0
- package/utils/color-yaml.d.ts +8 -0
- package/utils/color-yaml.js +86 -0
- package/utils/exec.d.ts +10 -0
- package/utils/exec.js +1 -1
- package/utils/git.d.ts +26 -0
- package/utils/git.js +65 -0
- package/utils/logger.d.ts +2 -2
- package/utils/logger.js +1 -1
- package/utils/package-filter.d.ts +135 -8
- package/utils/package-filter.js +204 -16
- package/utils/printable-config.d.ts +9 -1
- package/utils/printable-config.js +46 -1
- package/utils/progress-panel.d.ts +9 -0
- package/utils/progress-panel.js +23 -1
- package/utils/release-version.d.ts +1 -1
- package/utils/run-bin.d.ts +10 -0
- package/utils/run-bin.js +1 -1
- package/utils/run-options.d.ts +97 -0
- package/utils/run-options.js +81 -0
- package/utils/version-group.d.ts +31 -0
- package/utils/version-group.js +43 -0
- package/utils/version-stamp.d.ts +1 -1
- package/utils/version-stamp.js +1 -1
- package/commands/changed.command.d.ts +0 -3
- package/commands/changed.command.js +0 -29
- package/core/config.d.ts +0 -409
- package/core/config.js +0 -939
- package/core/extends-config.d.ts +0 -27
- package/core/extends-config.js +0 -89
- package/core/manifest.js +0 -150
- package/core/merge-config.js +0 -219
- package/core/package.d.ts +0 -83
- package/core/package.js +0 -106
- package/core/plugin.d.ts +0 -112
- package/core/plugin.js +0 -189
- package/core/workspace.d.ts +0 -68
- package/core/workspace.js +0 -83
- /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two members of the expression scope that answer questions about the filesystem: `file`, which
|
|
3
|
+
* says **where** something is, and `read`, which says what is **in** it.
|
|
4
|
+
*
|
|
5
|
+
* Both are built per directory - a package's own - so one `"[*]"` declaration asks each package
|
|
6
|
+
* about its own files. The parse cache is per *instance* and shared across those directories, so
|
|
7
|
+
* twenty packages reading one file parse it once.
|
|
8
|
+
*
|
|
9
|
+
* ```yaml
|
|
10
|
+
* "[*]":
|
|
11
|
+
* run:
|
|
12
|
+
* build:
|
|
13
|
+
* exec: 'tsc -b ${{ file.resolveFirst("tsconfig-build.json", "tsconfig.json") }}'
|
|
14
|
+
* version:
|
|
15
|
+
* stamp: '${{ read("pom.xml").getElementsByTagName("version")[0].textContent }}'
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* **Everything here is a query, and must stay one.** These run while a config resolves, which
|
|
19
|
+
* *every* command does - so anything that *did* something would do it on `rman list`, `rman info`
|
|
20
|
+
* and `rman config`, once per package, with nothing having asked.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ConfigFileScope {
|
|
23
|
+
protected readonly cache: Map<string, ConfigFileScope.CachedFile>;
|
|
24
|
+
/**
|
|
25
|
+
* One parsed file per path, kept against the identity of the bytes it came from.
|
|
26
|
+
*
|
|
27
|
+
* Passed in to share one cache across several instances; left out, the instance owns its own.
|
|
28
|
+
*/
|
|
29
|
+
constructor(cache?: Map<string, ConfigFileScope.CachedFile>);
|
|
30
|
+
/**
|
|
31
|
+
* The `file` member, resolving against `dirname`.
|
|
32
|
+
*
|
|
33
|
+
* @param dirname the package directory a relative path is resolved against
|
|
34
|
+
*/
|
|
35
|
+
fileScope(dirname: string): ConfigFileScope.FileScope;
|
|
36
|
+
/**
|
|
37
|
+
* The `read` member, resolving against `dirname`.
|
|
38
|
+
*
|
|
39
|
+
* @param dirname the package directory a relative path is resolved against
|
|
40
|
+
*/
|
|
41
|
+
readScope(dirname: string): ConfigFileScope.ReadFile;
|
|
42
|
+
/**
|
|
43
|
+
* Reads and parses one file, deeply frozen and memoized.
|
|
44
|
+
*
|
|
45
|
+
* Throws when the file is absent - `file.exists(p) ? read(p) : fallback` is the optional form, so
|
|
46
|
+
* there is no second function for it.
|
|
47
|
+
*/
|
|
48
|
+
protected readStructured(file: string, format?: ConfigFileScope.FileFormat): unknown;
|
|
49
|
+
/**
|
|
50
|
+
* What format a file name says it is. An unrecognized name is an error naming the four, rather
|
|
51
|
+
* than a guess at JSON.
|
|
52
|
+
*/
|
|
53
|
+
protected formatOf(file: string): ConfigFileScope.FileFormat;
|
|
54
|
+
/** Parses text in one of the four formats. Override to add another. */
|
|
55
|
+
protected parse(text: string, format: ConfigFileScope.FileFormat): unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Parses XML into a **DOM**, not a plain object.
|
|
58
|
+
*
|
|
59
|
+
* ```yaml
|
|
60
|
+
* version: '${{ read("pom.xml").getElementsByTagName("version")[0].textContent }}'
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
protected parseXml(text: string): unknown;
|
|
64
|
+
/** Where a relative target lands, and whether anything is there. */
|
|
65
|
+
protected _locate(dirname: string, target: string): {
|
|
66
|
+
path: string;
|
|
67
|
+
found: boolean;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export declare namespace ConfigFileScope {
|
|
71
|
+
/**
|
|
72
|
+
* Where a file is - the `file` member of the expression scope.
|
|
73
|
+
*
|
|
74
|
+
* Three read-only members, and that is the whole of it; see the class doc for why nothing here
|
|
75
|
+
* changes anything.
|
|
76
|
+
*/
|
|
77
|
+
interface FileScope {
|
|
78
|
+
/**
|
|
79
|
+
* The absolute path if it exists, **`''` if it does not** - so `a || b || c` picks the first one
|
|
80
|
+
* present, and so a miss never reaches the "nullish inside a string" guard that `undefined`
|
|
81
|
+
* would trip. Takes a relative path (against the package directory) or an absolute one.
|
|
82
|
+
*/
|
|
83
|
+
exists(target: string): string;
|
|
84
|
+
/** The absolute path, or **throws** - for a file whose absence is a mistake rather than a case
|
|
85
|
+
* to handle. The error names the config path holding the expression, like any other. */
|
|
86
|
+
resolve(target: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* The first of several that exists, or **throws** naming every candidate it tried:
|
|
89
|
+
*
|
|
90
|
+
* ```yaml
|
|
91
|
+
* exec: 'tsc -b ${{ file.resolveFirst("tsconfig-build.json", "tsconfig.json") }}'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
resolveFirst(...targets: string[]): string;
|
|
95
|
+
}
|
|
96
|
+
/** What `read` can parse. */
|
|
97
|
+
type FileFormat = 'json' | 'yaml' | 'ini' | 'xml';
|
|
98
|
+
/** `read(path)`, or `read(path, 'ini')` for a file whose name does not say what it is
|
|
99
|
+
* (`.npmrc`). */
|
|
100
|
+
type ReadFile = (target: string, format?: FileFormat) => unknown;
|
|
101
|
+
/** One parsed file, kept against the identity of the bytes it came from. */
|
|
102
|
+
interface CachedFile {
|
|
103
|
+
/** `mtimeNs:size`. */
|
|
104
|
+
stamp: string;
|
|
105
|
+
value: unknown;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
4
|
+
import ini from 'ini';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
/**
|
|
7
|
+
* The two members of the expression scope that answer questions about the filesystem: `file`, which
|
|
8
|
+
* says **where** something is, and `read`, which says what is **in** it.
|
|
9
|
+
*
|
|
10
|
+
* Both are built per directory - a package's own - so one `"[*]"` declaration asks each package
|
|
11
|
+
* about its own files. The parse cache is per *instance* and shared across those directories, so
|
|
12
|
+
* twenty packages reading one file parse it once.
|
|
13
|
+
*
|
|
14
|
+
* ```yaml
|
|
15
|
+
* "[*]":
|
|
16
|
+
* run:
|
|
17
|
+
* build:
|
|
18
|
+
* exec: 'tsc -b ${{ file.resolveFirst("tsconfig-build.json", "tsconfig.json") }}'
|
|
19
|
+
* version:
|
|
20
|
+
* stamp: '${{ read("pom.xml").getElementsByTagName("version")[0].textContent }}'
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* **Everything here is a query, and must stay one.** These run while a config resolves, which
|
|
24
|
+
* *every* command does - so anything that *did* something would do it on `rman list`, `rman info`
|
|
25
|
+
* and `rman config`, once per package, with nothing having asked.
|
|
26
|
+
*/
|
|
27
|
+
/* **Do not add `copy`, `write` or `mkdir`, however reasonable the request sounds.** It has already
|
|
28
|
+
* been tried, in the only way a function that does not exist can be: a shared config reaching for
|
|
29
|
+
* `file.copyMany(...)` made **every** rman command exit 1. The loud failure was the lucky outcome -
|
|
30
|
+
* had the member existed, `rman list` would have quietly copied files. Work goes in a step, which
|
|
31
|
+
* is the one thing rman runs on purpose, and a step can be a function too. */
|
|
32
|
+
export class ConfigFileScope {
|
|
33
|
+
cache;
|
|
34
|
+
/**
|
|
35
|
+
* One parsed file per path, kept against the identity of the bytes it came from.
|
|
36
|
+
*
|
|
37
|
+
* Passed in to share one cache across several instances; left out, the instance owns its own.
|
|
38
|
+
*/
|
|
39
|
+
/* **Per instance rather than per scope**, because the scope is built once *per package*: a cache
|
|
40
|
+
* living in one pass never helps across them, and twenty packages reading one shared file would
|
|
41
|
+
* parse it twenty times. */
|
|
42
|
+
constructor(cache = new Map()) {
|
|
43
|
+
this.cache = cache;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `file` member, resolving against `dirname`.
|
|
47
|
+
*
|
|
48
|
+
* @param dirname the package directory a relative path is resolved against
|
|
49
|
+
*/
|
|
50
|
+
fileScope(dirname) {
|
|
51
|
+
return {
|
|
52
|
+
exists: (target) => {
|
|
53
|
+
const { path: resolved, found } = this._locate(dirname, target);
|
|
54
|
+
return found ? resolved : '';
|
|
55
|
+
},
|
|
56
|
+
resolve: (target) => {
|
|
57
|
+
const { path: resolved, found } = this._locate(dirname, target);
|
|
58
|
+
if (!found) {
|
|
59
|
+
throw new Error(`file.resolve("${target}") found nothing at ${resolved}\n` +
|
|
60
|
+
` Use file.exists() instead if its absence is a case to handle rather than a mistake.`);
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
},
|
|
64
|
+
resolveFirst: (...targets) => {
|
|
65
|
+
if (!targets.length)
|
|
66
|
+
throw new Error('file.resolveFirst() needs at least one path');
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
const { path: resolved, found } = this._locate(dirname, target);
|
|
69
|
+
if (found)
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`file.resolveFirst() found none of: ${targets.map(t => `"${t}"`).join(', ')}\n Looked in ${dirname}.`);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `read` member, resolving against `dirname`.
|
|
78
|
+
*
|
|
79
|
+
* @param dirname the package directory a relative path is resolved against
|
|
80
|
+
*/
|
|
81
|
+
readScope(dirname) {
|
|
82
|
+
return (target, format) => {
|
|
83
|
+
if (typeof target !== 'string' || !target.trim()) {
|
|
84
|
+
throw new Error('read() needs a path - it was given ' + JSON.stringify(target));
|
|
85
|
+
}
|
|
86
|
+
return this.readStructured(path.resolve(dirname, target), format);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reads and parses one file, deeply frozen and memoized.
|
|
91
|
+
*
|
|
92
|
+
* Throws when the file is absent - `file.exists(p) ? read(p) : fallback` is the optional form, so
|
|
93
|
+
* there is no second function for it.
|
|
94
|
+
*/
|
|
95
|
+
/* **Memoized against the identity of the contents, not the path**: the key is `mtimeNs:size`.
|
|
96
|
+
* Both halves were measured.
|
|
97
|
+
*
|
|
98
|
+
* A stat rather than a re-read: `statSync` is 1.3µs where `readFileSync` + `JSON.parse` is 16.1µs
|
|
99
|
+
* on a 2KB manifest, so the check costs a thirteenth of what it saves.
|
|
100
|
+
*
|
|
101
|
+
* Keyed on the stat rather than held for the run, because **rman writes files while it runs** -
|
|
102
|
+
* `version` rewrites every bumped manifest and then re-interpolates its deferred hooks, and a
|
|
103
|
+
* path-keyed cache would hand those back as they were before the write. `mtimeNs` is nanoseconds,
|
|
104
|
+
* so a same-millisecond rewrite does not slip through.
|
|
105
|
+
*
|
|
106
|
+
* **Frozen once on the way in and shared**, rather than copied per caller: every package gets the
|
|
107
|
+
* same object, so a mutation would quietly change what the next one sees. A copy costs 5.6µs on
|
|
108
|
+
* every call where freezing costs ~1µs once, and it turns the mistake into a `TypeError` rather
|
|
109
|
+
* than an effect at a distance. */
|
|
110
|
+
readStructured(file, format) {
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = fs.statSync(file, { bigint: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error(`read("${path.basename(file)}") found nothing at ${file}\n` +
|
|
117
|
+
` Use file.exists() first if its absence is a case to handle rather than a mistake.`);
|
|
118
|
+
}
|
|
119
|
+
if (stat.isDirectory())
|
|
120
|
+
throw new Error(`read() was given a directory, not a file: ${file}`);
|
|
121
|
+
const stamp = `${stat.mtimeNs}:${stat.size}`;
|
|
122
|
+
const cached = this.cache.get(file);
|
|
123
|
+
if (cached?.stamp === stamp)
|
|
124
|
+
return cached.value;
|
|
125
|
+
const resolved = format ?? this.formatOf(file);
|
|
126
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
127
|
+
let value;
|
|
128
|
+
try {
|
|
129
|
+
value = this.parse(text, resolved);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
/** The parser's own message says what is wrong with the syntax but never which file it was
|
|
133
|
+
* reading - and an expression can name several. */
|
|
134
|
+
throw new Error(`read("${path.basename(file)}") could not parse ${file} as ${resolved}: ${e?.message}`, {
|
|
135
|
+
cause: e,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
deepFreeze(value);
|
|
139
|
+
this.cache.set(file, { stamp, value });
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* What format a file name says it is. An unrecognized name is an error naming the four, rather
|
|
144
|
+
* than a guess at JSON.
|
|
145
|
+
*/
|
|
146
|
+
/* The extension decides because the caller already wrote it - naming the parser as well would
|
|
147
|
+
* restate it and let the two disagree (`json("x.yml")`). A name that says nothing takes the
|
|
148
|
+
* explicit argument instead. */
|
|
149
|
+
formatOf(file) {
|
|
150
|
+
const ext = path.extname(file).toLowerCase();
|
|
151
|
+
if (ext === '.json')
|
|
152
|
+
return 'json';
|
|
153
|
+
if (ext === '.yml' || ext === '.yaml')
|
|
154
|
+
return 'yaml';
|
|
155
|
+
if (ext === '.ini')
|
|
156
|
+
return 'ini';
|
|
157
|
+
if (XML_EXTENSIONS.has(ext))
|
|
158
|
+
return 'xml';
|
|
159
|
+
throw new Error(`read() cannot tell what "${path.basename(file)}" is from its name.\n` +
|
|
160
|
+
` Name the format: read("${path.basename(file)}", "json" | "yaml" | "ini" | "xml").`);
|
|
161
|
+
}
|
|
162
|
+
/** Parses text in one of the four formats. Override to add another. */
|
|
163
|
+
parse(text, format) {
|
|
164
|
+
if (format === 'json')
|
|
165
|
+
return JSON.parse(text);
|
|
166
|
+
/** `load`, not `loadAll`: a multi-document stream has no single value to be, and js-yaml says
|
|
167
|
+
* so clearly enough ("expected a single document in the stream") to leave alone. */
|
|
168
|
+
if (format === 'yaml')
|
|
169
|
+
return yaml.load(text);
|
|
170
|
+
if (format === 'xml')
|
|
171
|
+
return this.parseXml(text);
|
|
172
|
+
return ini.parse(text);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parses XML into a **DOM**, not a plain object.
|
|
176
|
+
*
|
|
177
|
+
* ```yaml
|
|
178
|
+
* version: '${{ read("pom.xml").getElementsByTagName("version")[0].textContent }}'
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
/* **The asymmetry with the other three formats is the honest shape rather than an omission.** XML
|
|
182
|
+
* has no lossless object form: an element can repeat, carry attributes and hold text at the same
|
|
183
|
+
* time, so any flattening has to pick a convention (`$`? `_text`? array-or-not?) and be wrong for
|
|
184
|
+
* somebody. A DOM is the shape XML actually has.
|
|
185
|
+
*
|
|
186
|
+
* **Freezing a DOM is safe** - measured, not assumed: a frozen `@xmldom/xmldom` document still
|
|
187
|
+
* answers `getElementsByTagName` for a tag first asked about *after* the freeze (the
|
|
188
|
+
* live-collection case that would have broken it), reads attributes, resolves namespaces, walks
|
|
189
|
+
* `childNodes` and serialises back.
|
|
190
|
+
*
|
|
191
|
+
* **A malformed file must throw.** xmldom reports problems through a handler and otherwise
|
|
192
|
+
* carries on with whatever it salvaged - so without the check a truncated file came back as a
|
|
193
|
+
* half-parsed DOM and the expression reading it simply found nothing. `read()` throws for a
|
|
194
|
+
* broken JSON file; it has to throw for this one too. */
|
|
195
|
+
parseXml(text) {
|
|
196
|
+
const problems = [];
|
|
197
|
+
const doc = new DOMParser({
|
|
198
|
+
onError: (level, message) => {
|
|
199
|
+
if (level !== 'warning')
|
|
200
|
+
problems.push(message.split('\n')[0]);
|
|
201
|
+
},
|
|
202
|
+
}).parseFromString(text, 'text/xml');
|
|
203
|
+
if (problems.length)
|
|
204
|
+
throw new Error(problems[0]);
|
|
205
|
+
return doc;
|
|
206
|
+
}
|
|
207
|
+
/** Where a relative target lands, and whether anything is there. */
|
|
208
|
+
_locate(dirname, target) {
|
|
209
|
+
if (typeof target !== 'string' || !target.trim()) {
|
|
210
|
+
throw new Error('file.exists()/file.resolve() need a path - they were given ' + JSON.stringify(target));
|
|
211
|
+
}
|
|
212
|
+
const resolved = path.resolve(dirname, target);
|
|
213
|
+
return { path: resolved, found: fs.existsSync(resolved) };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** The XML family worth recognizing by name: a project file is XML whatever its extension calls
|
|
217
|
+
* itself, and `.csproj`/`.pom` are what a .NET or Maven repository actually holds. Anything else
|
|
218
|
+
* still reads with an explicit `read(p, 'xml')`. */
|
|
219
|
+
const XML_EXTENSIONS = new Set(['.xml', '.csproj', '.vbproj', '.fsproj', '.props', '.targets', '.nuspec', '.plist']);
|
|
220
|
+
function deepFreeze(value) {
|
|
221
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
222
|
+
return;
|
|
223
|
+
Object.freeze(value);
|
|
224
|
+
for (const item of Object.values(value))
|
|
225
|
+
deepFreeze(item);
|
|
226
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import vm from 'node:vm';
|
|
2
|
+
import type { ConfigScope } from '../../interfaces/config-scope.interface.js';
|
|
3
|
+
import type { Resolved } from '../../interfaces/rman-config.interface.js';
|
|
4
|
+
import { type PreviousValue } from './merge-config.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a config's values - every `${{ ... }}` expression and every value function - against a
|
|
7
|
+
* scope.
|
|
8
|
+
*
|
|
9
|
+
* Where `ConfigReader` answers *what the files say*, this answers *what those words evaluate to*.
|
|
10
|
+
* It takes a `ConfigScope` rather than a package, so whoever holds the repository builds the scope;
|
|
11
|
+
* `Repository.configScope` is that today.
|
|
12
|
+
*
|
|
13
|
+
* Two passes: {@link interpolate} resolves everything the scope can answer now, and
|
|
14
|
+
* {@link resolveDeferred} resolves what `defer(...)` left for later.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ConfigInterpolator {
|
|
17
|
+
/** How long one expression may run, in milliseconds. Raise it for a repository with genuinely
|
|
18
|
+
* slow `read()` calls. */
|
|
19
|
+
protected readonly expressionTimeout: number;
|
|
20
|
+
/** The file the key being walked was written in, for an error message to name. */
|
|
21
|
+
protected currentOrigin: string | undefined;
|
|
22
|
+
/** How many `defer(...)` calls have been rewritten so far. */
|
|
23
|
+
protected deferCount: number;
|
|
24
|
+
/**
|
|
25
|
+
* Pass one: resolves everything the scope can answer now, and leaves `defer(...)` for
|
|
26
|
+
* {@link resolveDeferred}.
|
|
27
|
+
*
|
|
28
|
+
* A string that is *nothing but* one expression keeps that value's own type -
|
|
29
|
+
* `"${{ pkg.private }}"` is a boolean, not the word `true`. Embedded in surrounding text it is
|
|
30
|
+
* stringified, and a nullish result there is an error rather than the word `undefined`.
|
|
31
|
+
*
|
|
32
|
+
* Returns `Resolved<T>`: every value function has been called, so they are gone from the type.
|
|
33
|
+
*/
|
|
34
|
+
interpolate<T>(args: ConfigInterpolator.InterpolateArgs<T>): Resolved<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Pass two: resolves what pass one deferred, against a scope that can now answer it.
|
|
37
|
+
*
|
|
38
|
+
* `version` is the case it exists for - `${{ defer(pkg.targetVersion) }}` in a `version` hook
|
|
39
|
+
* names a version that does not exist while the config is being read.
|
|
40
|
+
*
|
|
41
|
+
* Only the keys pass one marked are walked; a `${{` that arrived any other way is left exactly as
|
|
42
|
+
* it is. A string handed over on its own has no containing object to carry a mark, so it is
|
|
43
|
+
* interpolated outright - the caller passing one has already decided.
|
|
44
|
+
*/
|
|
45
|
+
resolveDeferred<T>(args: ConfigInterpolator.ResolveDeferredArgs<T>): T;
|
|
46
|
+
/** Whether anything under `config` is waiting for {@link resolveDeferred} - `false` for every
|
|
47
|
+
* config that never writes `defer(...)`, so the second pass can be skipped. */
|
|
48
|
+
hasDeferred(config: unknown): boolean;
|
|
49
|
+
/** The V8 context an expression is evaluated in - a clean scope holding only `scope`'s bindings. */
|
|
50
|
+
protected _createContext(scope: ConfigScope): vm.Context;
|
|
51
|
+
protected _walk(value: unknown, scope: ConfigScope, context: vm.Context, at: Path, skip: string[]): any;
|
|
52
|
+
/**
|
|
53
|
+
* Runs `body` with `vars` scoped to this node: a fresh copy at every level, with the node's own
|
|
54
|
+
* `vars` block - if it declares one - merged **per key** over what the level above resolved to,
|
|
55
|
+
* so redeclaring one var keeps the rest.
|
|
56
|
+
*
|
|
57
|
+
* The node's own block is resolved against the outer scope before being installed, so
|
|
58
|
+
* `vars: { out: '${{ vars.x }}/dist' }` refines the `x` it inherits rather than reading itself.
|
|
59
|
+
*/
|
|
60
|
+
protected _withScopedVars<T>(node: Record<string, unknown>, scope: ConfigScope, context: vm.Context, at: Path, skip: string[], body: () => T): T;
|
|
61
|
+
/**
|
|
62
|
+
* Walks one key with `value` bound to what the layers underneath resolved to.
|
|
63
|
+
*
|
|
64
|
+
* Always bound, even with nothing underneath - see {@link _previousValue} for what it is then.
|
|
65
|
+
* The chain resolves bottom-up, so a layer deriving from a layer that itself derived from
|
|
66
|
+
* something is handed the finished value rather than a half-resolved expression.
|
|
67
|
+
*/
|
|
68
|
+
protected _walkWithPrevious(item: unknown, previous: PreviousValue | undefined, scope: ConfigScope, context: vm.Context, at: Path, skip: string[]): unknown;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a function at `at` is **code** - a step to run later, or part of a plugin - rather than
|
|
71
|
+
* a value to compute now.
|
|
72
|
+
*
|
|
73
|
+
* Array indices are dropped before matching, so a function inside a *list* of steps is still a
|
|
74
|
+
* step; `*` in a `STEP_PATHS` entry matches any one segment (`run.<script>.exec`).
|
|
75
|
+
*/
|
|
76
|
+
protected _isCodePath(at: Path): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Calls a **value** function: the JS spelling of a `${{ }}` expression, answering the same
|
|
79
|
+
* question at the same moment.
|
|
80
|
+
*
|
|
81
|
+
* It is handed one object carrying everything an expression can name, plus `value` - what this
|
|
82
|
+
* key resolved to in the layers underneath.
|
|
83
|
+
*
|
|
84
|
+
* A value function **computes and returns; it must never act.** This runs while the config
|
|
85
|
+
* resolves, which *every* command does - so one that writes a file writes it on `rman list`,
|
|
86
|
+
* `rman info` and `rman config` too, once per package, with nothing having asked. Work goes in a
|
|
87
|
+
* step.
|
|
88
|
+
*/
|
|
89
|
+
protected _callValueFn(fn: (arg: unknown) => unknown, context: vm.Context, at: Path): unknown;
|
|
90
|
+
protected _interpolateString(value: string, context: vm.Context, at: Path): unknown;
|
|
91
|
+
/** Evaluates one expression, rewriting any `defer(...)` in it first. A failure names the config
|
|
92
|
+
* path and the expression. */
|
|
93
|
+
protected _evaluate(expr: string, source: string, context: vm.Context, at: Path): unknown;
|
|
94
|
+
/**
|
|
95
|
+
* Turns `defer(<inner>)` into a string literal holding `${{ <inner> }}`, so the expression around
|
|
96
|
+
* it resolves now and the deferred part survives as an expression for pass two.
|
|
97
|
+
*
|
|
98
|
+
* ```
|
|
99
|
+
* '_' + defer(pkg.targetVersion) + '-' + pkg.name
|
|
100
|
+
* '_' + "${{ pkg.targetVersion }}" + '-' + pkg.name // rewritten
|
|
101
|
+
* _${{ pkg.targetVersion }}-mypackage // evaluated
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* `defer` is a marker the source is rewritten around, not a function in scope - so
|
|
105
|
+
* `defer(() => x)` is not a thing to write. A nested `defer` is refused.
|
|
106
|
+
*/
|
|
107
|
+
protected _rewriteDefer(expr: string, source: string, at: Path): string;
|
|
108
|
+
/** The text between `defer(` and the parenthesis that closes it. */
|
|
109
|
+
protected _findDeferArgument(expr: string, open: number, source: string, at: Path): string;
|
|
110
|
+
/** Records which of `node`'s keys deferred, invisibly to anything but pass two. */
|
|
111
|
+
protected _markDeferred(node: Record<string, unknown>, keys: Record<string, true>): void;
|
|
112
|
+
/** Pass two's walk: only the keys pass one marked, so a `${{` that arrived from anywhere else is
|
|
113
|
+
* left exactly as it is. */
|
|
114
|
+
protected _walkDeferred(value: unknown, context: vm.Context, at: Path): unknown;
|
|
115
|
+
protected _withOrigin<T>(origin: string | undefined, body: () => T): T;
|
|
116
|
+
/** `"version.commitMessage" (.rmanrc.yml)`, or just the path when nothing recorded a file. The
|
|
117
|
+
* file is relative to the working directory when it sits inside it. */
|
|
118
|
+
protected _describeAt(at: Path): string;
|
|
119
|
+
/**
|
|
120
|
+
* What a layer deriving from the one below it reads as `value`.
|
|
121
|
+
*
|
|
122
|
+
* Spreads as empty when nothing below set the key, so `[...value, 'x']` needs no guard. Used as a
|
|
123
|
+
* string or a number it throws, naming the key and what to write instead.
|
|
124
|
+
*/
|
|
125
|
+
protected _previousValue(raw: unknown, at: Path): unknown;
|
|
126
|
+
/** The one sentence both `value` refusals share: what was asked for, why it cannot be done, what
|
|
127
|
+
* to write instead. */
|
|
128
|
+
protected _unusable(at: Path, because: string, instead: string): Error;
|
|
129
|
+
}
|
|
130
|
+
/** Where a value sits in the config, as the walk builds it up - a string key or an array index.
|
|
131
|
+
* A module-level type rather than a member of the namespace below, so the class's own signatures
|
|
132
|
+
* can name it without shadowing the namespace's copy of the name. */
|
|
133
|
+
type Path = (string | number)[];
|
|
134
|
+
export declare namespace ConfigInterpolator {
|
|
135
|
+
interface InterpolateArgs<T = unknown> {
|
|
136
|
+
/** The config, or a fragment of it. */
|
|
137
|
+
config: T;
|
|
138
|
+
/** Everything an expression can name: `pkg`, `repository`, `file`, `env`, `semver`, `path`,
|
|
139
|
+
* `git`. Built by whoever holds the repository - see the class doc for why it is not a
|
|
140
|
+
* `Package`. */
|
|
141
|
+
scope: ConfigScope;
|
|
142
|
+
/**
|
|
143
|
+
* Where `config` sits in the whole config, for a caller handing over a **fragment**.
|
|
144
|
+
*
|
|
145
|
+
* Optional in the type and required in practice for a fragment: the path is what decides
|
|
146
|
+
* whether a function is a value to compute or a step to leave alone (`STEP_PATHS`). Without it
|
|
147
|
+
* a fragment starts at the root and matches nothing, so a function in a `version` hook was
|
|
148
|
+
* called while the hook was being *prepared* - it failed inside the user's own code with
|
|
149
|
+
* `path.join` receiving undefined.
|
|
150
|
+
*/
|
|
151
|
+
at?: Path;
|
|
152
|
+
/** Config paths to leave entirely untouched, subtree and all. */
|
|
153
|
+
skip?: string[];
|
|
154
|
+
}
|
|
155
|
+
interface ResolveDeferredArgs<T = unknown> {
|
|
156
|
+
/** What `interpolate` returned - the marks it left are what this walks. */
|
|
157
|
+
config: T;
|
|
158
|
+
/** The scope with whatever pass one could not bind, `pkg.targetVersion` being the case this
|
|
159
|
+
* exists for. */
|
|
160
|
+
scope: ConfigScope;
|
|
161
|
+
at?: Path;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export {};
|