rman-node 1.1.1

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/augmentation/manifest.augmentation.d.ts +17 -0
  4. package/augmentation/manifest.augmentation.js +129 -0
  5. package/augmentation/rman.augmentation.d.ts +45 -0
  6. package/augmentation/rman.augmentation.js +1 -0
  7. package/augmentation/run.augmentation.d.ts +15 -0
  8. package/augmentation/run.augmentation.js +61 -0
  9. package/augmentation/system-info.augmentation.d.ts +26 -0
  10. package/augmentation/system-info.augmentation.js +79 -0
  11. package/augmentation/workspace.augmentation.d.ts +18 -0
  12. package/augmentation/workspace.augmentation.js +49 -0
  13. package/commands/ci.command.d.ts +9 -0
  14. package/commands/ci.command.js +35 -0
  15. package/commands/clean.command.d.ts +6 -0
  16. package/commands/clean.command.js +32 -0
  17. package/commands/publish.command.d.ts +9 -0
  18. package/commands/publish.command.js +228 -0
  19. package/index.d.ts +31 -0
  20. package/index.js +100 -0
  21. package/interfaces/rman-config.interface.d.ts +80 -0
  22. package/interfaces/rman-config.interface.js +7 -0
  23. package/package.json +54 -0
  24. package/services/ci.service.d.ts +38 -0
  25. package/services/ci.service.js +201 -0
  26. package/services/clean.service.d.ts +51 -0
  27. package/services/clean.service.js +235 -0
  28. package/services/publish.service.d.ts +77 -0
  29. package/services/publish.service.js +258 -0
  30. package/services/version-plan.service.d.ts +50 -0
  31. package/services/version-plan.service.js +67 -0
  32. package/utils/npm-run-path.d.ts +21 -0
  33. package/utils/npm-run-path.js +36 -0
  34. package/utils/npm-view.d.ts +15 -0
  35. package/utils/npm-view.js +28 -0
  36. package/utils/workspace-range.d.ts +26 -0
  37. package/utils/workspace-range.js +28 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Panates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # rman-node
2
+
3
+ Node.js support for [rman](https://github.com/panates/rman).
4
+
5
+ rman's core is about repositories - packages, versions, changelogs, releases, branches. Anything
6
+ that only means something because the repository happens to be a Node one lives here, so the core
7
+ stays usable by a repository in any language.
8
+
9
+ Full API reference: **[docs/node.md](https://github.com/panates/rman/blob/main/docs/node.md)**.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -D rman-node
15
+ ```
16
+
17
+ ```yaml
18
+ # .rmanrc.yml
19
+ plugins: ['rman-node']
20
+ ```
21
+
22
+ A plugin that cannot be loaded is an error, not a skip: silently losing `rman publish` is worse
23
+ than not starting.
24
+
25
+ ## Commands
26
+
27
+ These three commands do not exist without this package - `rman clean` is `Unknown argument: clean`
28
+ in a repository that names no plugin.
29
+
30
+ ### `rman publish`
31
+
32
+ Publishes every package to its configured registry - `npm` by default, or whatever each package's
33
+ own `.rmanrc "publish.target"` says (`"npm"`, `"docker"`, or both). Each target decides for itself
34
+ whether the current version is already out there: `npm view` on the npm side, `docker manifest
35
+ inspect` on the docker side.
36
+
37
+ ```bash
38
+ rman publish # show the plan, then ask for confirmation
39
+ rman publish --yes # publish immediately, no confirmation
40
+ rman publish --dry-run # only show the plan, never publish
41
+ rman publish --access public # required for a new scoped package
42
+ rman publish --tag next
43
+ rman publish --otp 123456
44
+ rman publish --registry https://registry.example.com --userconfig ./ci.npmrc
45
+ rman publish --package-manager pnpm
46
+ rman publish --target docker # only the packages configured for the "docker" target
47
+ ```
48
+
49
+ A `"workspace:*"`/`"workspace:^"`/`"workspace:~"` dependency range is automatically rewritten to a
50
+ real, registry-consumable range immediately before each package's publish, and restored right
51
+ after - see [docs/node.md#publishservice](https://github.com/panates/rman/blob/main/docs/node.md#publishservice).
52
+
53
+ A package opts into building/pushing a Docker image via `.rmanrc "publish.target": ["docker"]` plus
54
+ a `"publish.docker"` block (`image`, `platforms`, `buildContexts`, `buildArgs`, ...) - see
55
+ [docs/cli/publish.md](https://github.com/panates/rman/blob/main/docs/cli/publish.md#docker-publishing-publishdocker).
56
+
57
+ ### `rman ci`
58
+
59
+ Deletes `node_modules` and any lockfile in every package (or runs the package's own `"ci"` script
60
+ instead, if it defines one), then installs once at the root.
61
+
62
+ ```bash
63
+ rman ci
64
+ rman ci --package-manager pnpm
65
+ ```
66
+
67
+ ### `rman clean`
68
+
69
+ Removes compiled TypeScript output (`.js`/`.js.map`/`.d.ts` under `src`/`test`, plus any
70
+ `*.tsbuildinfo`) and whatever `.rmanrc clean.include`/`clean.exclude` configures. Never touches
71
+ `node_modules` - that's `ci`'s job.
72
+
73
+ ```bash
74
+ rman clean
75
+ rman clean --dry-run # preview what would be removed
76
+ ```
77
+
78
+ Every one of its built-in behaviours is a **TypeScript** fact, which is why it is here and not in
79
+ the core: nothing in it would fire for a Cargo or Go repository, and both ship a `clean` of their
80
+ own. A `.d.ts` with no matching `.ts` beside it is left alone - that is a hand-written declaration,
81
+ not build output.
82
+
83
+ ## Config keys
84
+
85
+ Three `.rmanrc` keys come from this package, and they reach rman's own `RmanConfig` type by
86
+ declaration merging - so `pkg.config.clean` is typed where it is read, with no cast:
87
+
88
+ | Key | Level | |
89
+ | --- | --- | --- |
90
+ | `packageManager` | root only | Which package manager `ci`/`publish` shell out to, and whose version `info` reports. Default `npm`. |
91
+ | `clean` | per package | `include`/`exclude` globs beyond TypeScript's own output, plus `skip`. |
92
+ | `publish.directory` | per package | Where this package's publishable output lives, relative to its own directory. |
93
+
94
+ `RmanNodeConfig` is the name a config author annotates with, and importing its `defineConfig` is
95
+ what carries the augmentation:
96
+
97
+ ```js
98
+ // .rmanrc.mjs
99
+ import { defineConfig } from 'rman-node';
100
+
101
+ export default defineConfig({
102
+ plugins: ['rman-node'],
103
+ packageManager: 'pnpm',
104
+ '[ws:*]': { clean: { include: 'build' }, publish: { directory: 'build' } },
105
+ });
106
+ ```
107
+
108
+ The JSON and YAML forms of the config carry no type - rman ships no JSON Schema, because a schema
109
+ cannot describe keys a plugin contributes. See
110
+ [docs/rman.md#editor-support-types](https://github.com/panates/rman/blob/main/docs/rman.md#editor-support-types).
111
+
112
+ ## Seams and augmentations
113
+
114
+ Beyond the commands, this package answers the questions rman's core deliberately has no answer to:
115
+ what a package *is* (`package.json`), which directories are packages (`workspaces`), how a version
116
+ is planned, what `pre<script>`/`post<script>` mean, and that `node_modules/.bin` belongs on a child
117
+ process's PATH. Without it a repository has **no manifest reader at all** - which is the point:
118
+ another ecosystem supplies its own through the same seams rather than working around npm's.
119
+
120
+ Some core behaviour is neutral by default and this package turns it on, because installing it is
121
+ itself the statement that the repository is a Node one.
122
+
123
+ **`SystemInfo`** (`rman info`): the core service asks about a package manager only when told to - a
124
+ repository in another language reporting "npm: Not Found" is a wrong answer, not a missing feature.
125
+ This package makes npm the default and adds its own version to the report:
126
+
127
+ ```
128
+ # without the plugin # with it
129
+ Binaries: Binaries:
130
+ Node : 24.15.0 Node : 24.15.0
131
+ npm : 11.12.1
132
+ npmPackages:
133
+ rman-node : ...
134
+ rman : ...
135
+ ```
136
+
137
+ `.rmanrc "packageManager"` still decides *which* one; the augmentation only decides that there is
138
+ one at all.
139
+
140
+ ## Not here
141
+
142
+ **Docker publishing stayed in the core** - any language's project can publish an image. What is
143
+ still wrong is that `publish` is what *drives* it, so a non-Node repository has to install this
144
+ plugin to reach `publish --target docker`. Fixing that means making a publish target something a
145
+ plugin contributes to a core `publish`.
146
+
147
+ ## Licence
148
+
149
+ MIT
@@ -0,0 +1,17 @@
1
+ import { type ManifestProvider } from 'rman';
2
+ /**
3
+ * `package.json` as rman's manifest: where an npm package's name and version are written.
4
+ *
5
+ * This is the deepest npm assumption that used to be in rman's core - `Package.name`,
6
+ * `Package.version`, `Package.json` and `Package.isPrivate` all read this file directly, which
7
+ * meant every command that asked a package what it was called was asking npm.
8
+ *
9
+ * The version scheme is left unset, so packages get the core's semver default: npm versions *are*
10
+ * semver, and saying so again here would only be a second place for the two to disagree.
11
+ */
12
+ export declare const packageJsonManifest: ManifestProvider;
13
+ /** npm's four dependency fields. Was `rman`'s `DEPENDENCY_KEYS`, which made the core carry npm's
14
+ * field names - `publish` imports it from here now. */
15
+ export declare const DEPENDENCY_KEYS: readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
16
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
17
+ export declare function augmentManifest(): void;
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Manifest, stampVersionConstant } from 'rman';
4
+ import { npmViewVersion } from '../utils/npm-view.js';
5
+ import { parseWorkspaceRange } from '../utils/workspace-range.js';
6
+ /**
7
+ * `package.json` as rman's manifest: where an npm package's name and version are written.
8
+ *
9
+ * This is the deepest npm assumption that used to be in rman's core - `Package.name`,
10
+ * `Package.version`, `Package.json` and `Package.isPrivate` all read this file directly, which
11
+ * meant every command that asked a package what it was called was asking npm.
12
+ *
13
+ * The version scheme is left unset, so packages get the core's semver default: npm versions *are*
14
+ * semver, and saying so again here would only be a second place for the two to disagree.
15
+ */
16
+ export const packageJsonManifest = {
17
+ /** The ecosystem, not the file - this is what every package it reads reports as
18
+ * `pkg.provider === 'node'`. `fileName` below already says `package.json`. */
19
+ name: 'node',
20
+ fileName: 'package.json',
21
+ read(dir) {
22
+ const file = path.join(dir, 'package.json');
23
+ if (!fs.existsSync(file))
24
+ return undefined;
25
+ const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
26
+ return {
27
+ /** A `package.json` with no `name` is unusual but legal, and `info` prints such a package
28
+ * rather than refusing it - so the directory name stands in, as it does for a package with
29
+ * no manifest at all. */
30
+ name: typeof raw?.name === 'string' && raw.name ? raw.name : path.basename(dir),
31
+ version: typeof raw?.version === 'string' && raw.version ? raw.version : '0.0.0',
32
+ private: !!raw?.private,
33
+ raw: raw ?? {},
34
+ };
35
+ },
36
+ /**
37
+ * npm's four dependency fields, and only the entries naming a package of this repository -
38
+ * an external dependency is not an edge in rman's graph.
39
+ *
40
+ * Matched by name, which is npm's own identifier and unique by construction; an ecosystem where
41
+ * that does not hold resolves its own way, which is why this is the provider's job.
42
+ */
43
+ dependencies(manifest, candidates) {
44
+ const declared = Object.assign({}, ...DEPENDENCY_KEYS.map(key => manifest.raw[key]));
45
+ const byName = new Map(candidates.map(p => [p.name, p]));
46
+ const result = [];
47
+ for (const name of Object.keys(declared)) {
48
+ const pkg = byName.get(name);
49
+ if (pkg && !result.includes(pkg))
50
+ result.push(pkg);
51
+ }
52
+ return result;
53
+ },
54
+ /** npm's `@scope/name`. A name with no `/` has no scope and is its own unscoped form; the
55
+ * *last* `/` splits it, so `@scope/a/b` keeps `@scope/a` as the scope the registry would. */
56
+ splitName(name) {
57
+ const at = name.lastIndexOf('/');
58
+ return at > 0 ? { scope: name.slice(0, at), unscopedName: name.slice(at + 1) } : { unscopedName: name };
59
+ },
60
+ /**
61
+ * Rewrites a sibling's range in all four fields after it was bumped.
62
+ *
63
+ * **A bare `"workspace:*"`/`"^"`/`"~"` selector is left alone**, and that is the load-bearing
64
+ * case: it resolves to the dependency's *current* version at publish time (see
65
+ * `resolveWorkspaceRange`), so rewriting it would replace a live reference with a frozen one.
66
+ * Only an explicit version after `workspace:` needs bumping, like a plain range.
67
+ */
68
+ updateDependencyVersions(manifest, bumped) {
69
+ const versionByName = new Map([...bumped].map(([pkg, version]) => [pkg.name, version]));
70
+ for (const depKey of DEPENDENCY_KEYS) {
71
+ const deps = manifest.raw[depKey];
72
+ if (!deps)
73
+ continue;
74
+ for (const depName of Object.keys(deps)) {
75
+ const to = versionByName.get(depName);
76
+ if (!to)
77
+ continue;
78
+ const workspace = parseWorkspaceRange(deps[depName]);
79
+ if (workspace) {
80
+ if (workspace.selector === 'explicit')
81
+ deps[depName] = `workspace:^${to}`;
82
+ continue;
83
+ }
84
+ deps[depName] = '^' + to;
85
+ }
86
+ }
87
+ },
88
+ /**
89
+ * What the npm registry says this package's current version is - used *only* by
90
+ * `detectChangeHash`, to guess a tag name for a package that has no git tag yet. See
91
+ * `ManifestProvider.publishedVersion` for why that is not a "has this been published" check.
92
+ *
93
+ * No `--registry`/`--userconfig`: a bare `npm view` run in the package's own directory already
94
+ * honours the repository's `.npmrc`, and the flags exist for `publish`'s CLI overrides, which
95
+ * this path has none of.
96
+ */
97
+ publishedVersion(pkg) {
98
+ return npmViewVersion(pkg.name, pkg.dirname);
99
+ },
100
+ /**
101
+ * npm's source shape: a quoted string assigned to an identifier, which is what a `.ts`/`.js`
102
+ * version constant looks like. `constant` comes from the repository's own `version.stamp` entry,
103
+ * for a file whose identifier is not literally `version`.
104
+ *
105
+ * One line, because `stampVersionConstant` is exported for it - the *pattern* is shared by most
106
+ * languages, while *choosing* it is this ecosystem's call. A provider for a format with no
107
+ * identifier at all (a `pom.xml`) would ignore `constant` and match its own way.
108
+ */
109
+ stampVersion(file, content, version, options) {
110
+ return stampVersionConstant(content, version, options?.constant);
111
+ },
112
+ write(dir, manifest) {
113
+ /**
114
+ * The version is written back into `raw` before serializing, because `raw` is the document and
115
+ * `manifest.version` is rman's view of one field in it. Writing only the view would produce a
116
+ * file whose `version` never changed; writing only `raw` would leave the two disagreeing for
117
+ * anything still holding the manifest.
118
+ */
119
+ const raw = { ...manifest.raw, version: manifest.version };
120
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(raw, undefined, 2) + '\n', 'utf-8');
121
+ },
122
+ };
123
+ /** npm's four dependency fields. Was `rman`'s `DEPENDENCY_KEYS`, which made the core carry npm's
124
+ * field names - `publish` imports it from here now. */
125
+ export const DEPENDENCY_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
126
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
127
+ export function augmentManifest() {
128
+ Manifest.addProvider(packageJsonManifest);
129
+ }
@@ -0,0 +1,45 @@
1
+ import type { NodeConfigKeys, RmanNodeConfig } from '../interfaces/rman-config.interface.js';
2
+ import type { CiService } from '../services/ci.service.js';
3
+ /**
4
+ * **Every type this plugin adds to rman, in one `declare module` block.**
5
+ *
6
+ * One block, and that is not tidiness: a *second* `declare module 'rman'` anywhere in this package
7
+ * silently disables the first. Measured - moving the config keys into their own file left
8
+ * `SystemInfo.PackageManager` unresolved at four call sites in `system-info.augmentation.ts`, with
9
+ * no error pointing at the cause. So a new augmentation goes **here**, beside the others, rather
10
+ * than next to the code it belongs to.
11
+ *
12
+ * Type-only. The runtime half of each augmentation lives with its own subject
13
+ * (`augmentSystemInfo()`, `augmentManifest()`, ...); this file is imported by the plugin's entry
14
+ * point purely so a consumer's compiler loads it.
15
+ */
16
+ declare module 'rman' {
17
+ /**
18
+ * The `.rmanrc` keys that only mean something in a Node repository - merged into the core's own
19
+ * key list, so `pkg.config.clean` is typed wherever it is read (`CleanService` included) without
20
+ * a cast, and without the core declaring a key it knows nothing about.
21
+ *
22
+ * `WithAppend<RmanConfigKeys>` is a mapped type evaluated where it is used, so `+clean` comes
23
+ * along on its own.
24
+ */
25
+ interface RmanConfigKeys extends NodeConfigKeys {
26
+ }
27
+ namespace RmanConfig {
28
+ /** The npm half of `publish`. `target`, `skip` and `docker` stay in the core: the first two are
29
+ * read by `list` and by every target's own plan, and Docker publishing is not Node's. */
30
+ interface PublishOptionsKeys extends RmanNodeConfig.PublishOptions {
31
+ }
32
+ }
33
+ namespace SystemInfo {
34
+ type PackageManager = CiService.PackageManager;
35
+ interface Options {
36
+ /**
37
+ * Report this package manager's version under `Binaries`, plus the `npmPackages` sections.
38
+ *
39
+ * Defaults to `.rmanrc "packageManager"` (read off `Options.repository`), then to `npm` -
40
+ * this package being installed is itself the statement that the repository is a Node one.
41
+ */
42
+ packageManager?: PackageManager;
43
+ }
44
+ }
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { RunService } from 'rman';
2
+ /**
3
+ * Teaches `run` about `package.json#scripts`.
4
+ *
5
+ * rman's core resolves a script from `.rmanrc` alone - `before`/`exec`/`after`. That a script might
6
+ * *also* live in `package.json`, that `prebuild` and `postbuild` run around `build`, and that
7
+ * `a && b` is two steps rather than one, are all facts about npm, so they are here.
8
+ *
9
+ * Registered as a `RunService` step source rather than by wrapping anything: the npm part of `run`
10
+ * is inside step resolution, not at its entry point, so there is nothing a wrapper could reach.
11
+ * The core decides what to do with what this returns - see `getScriptSteps` for the precedence.
12
+ */
13
+ export declare const packageJsonSteps: RunService.StepSource;
14
+ /** Registers the source with rman's `run`. Called by the plugin entry point, once. */
15
+ export declare function augmentRun(): void;
@@ -0,0 +1,61 @@
1
+ import parseNpmScript from '@netlify/parse-npm-script';
2
+ import { RunService } from 'rman';
3
+ /**
4
+ * Teaches `run` about `package.json#scripts`.
5
+ *
6
+ * rman's core resolves a script from `.rmanrc` alone - `before`/`exec`/`after`. That a script might
7
+ * *also* live in `package.json`, that `prebuild` and `postbuild` run around `build`, and that
8
+ * `a && b` is two steps rather than one, are all facts about npm, so they are here.
9
+ *
10
+ * Registered as a `RunService` step source rather than by wrapping anything: the npm part of `run`
11
+ * is inside step resolution, not at its entry point, so there is nothing a wrapper could reach.
12
+ * The core decides what to do with what this returns - see `getScriptSteps` for the precedence.
13
+ */
14
+ export const packageJsonSteps = (pkg, script) => {
15
+ const scripts = pkg.manifest.raw?.scripts;
16
+ if (!scripts || typeof scripts !== 'object')
17
+ return undefined;
18
+ const declared = (name) => typeof scripts[name] === 'string' && !!scripts[name];
19
+ if (!declared(script) && !declared('pre' + script) && !declared('post' + script))
20
+ return undefined;
21
+ /**
22
+ * `parseNpmScript` expands npm's own lifecycle - `pre<script>`, the script, `post<script>` - and
23
+ * splits each on `&&` into the commands npm would run in sequence. The placeholder matters: it
24
+ * asks for a script the package may not declare itself (it may have only a `prebuild`), and
25
+ * without something there the parser reports the whole chain as absent.
26
+ */
27
+ const json = { ...pkg.manifest.raw, scripts: { ...scripts } };
28
+ json.scripts[script] = json.scripts[script] || PLACEHOLDER;
29
+ const info = parseNpmScript(json, 'npm run ' + script);
30
+ if (!info?.raw?.length)
31
+ return undefined;
32
+ const slots = { before: [], exec: [], after: [] };
33
+ for (const step of info.steps) {
34
+ const slot = slotOf(step.name, script);
35
+ if (!slot)
36
+ continue;
37
+ for (const command of Array.isArray(step.parsed) ? step.parsed : [step.parsed]) {
38
+ if (command === PLACEHOLDER)
39
+ continue;
40
+ slots[slot].push(command);
41
+ }
42
+ }
43
+ return slots;
44
+ };
45
+ /** Registers the source with rman's `run`. Called by the plugin entry point, once. */
46
+ export function augmentRun() {
47
+ RunService.addStepSource(packageJsonSteps);
48
+ }
49
+ /** npm's lifecycle names, mapped onto rman's three slots. */
50
+ function slotOf(stepName, script) {
51
+ if (stepName === 'pre' + script)
52
+ return 'before';
53
+ if (stepName === script)
54
+ return 'exec';
55
+ if (stepName === 'post' + script)
56
+ return 'after';
57
+ return undefined;
58
+ }
59
+ /** Stands in for a script the package does not declare. `#` is a shell comment, so were it ever to
60
+ * reach a shell it would do nothing - but it is filtered out above instead. */
61
+ const PLACEHOLDER = '#';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Adds the npm half of `SystemInfo` to rman's own types.
3
+ *
4
+ * The core's `Options` carries nothing language-specific on purpose, so `packageManager` is not a
5
+ * setting it hid behind a flag - it is a setting it does not have. Declaration merging is what lets
6
+ * this package add it as if it had always been there, `SystemInfo.PackageManager` included, without
7
+ * the core naming npm anywhere.
8
+ *
9
+ * The union itself is `CiService.PackageManager`, not a second copy: `ci` and `publish` already
10
+ * shell out to one of these, and two lists of the same four names drift the moment a fifth appears.
11
+ */
12
+ /** The import above is aliased precisely so this block can use the real name: declaration merging
13
+ * keys off the module specifier, not off any local binding, so `namespace SystemInfo` here *is*
14
+ * rman's - and nothing shadows anything. */
15
+ /** The `SystemInfo` types this file's wrapper relies on are declared in
16
+ * [`rman.augmentation.ts`](rman.augmentation.ts), with the rest of this plugin's - one
17
+ * `declare module` block per package, or the others stop applying. */
18
+ /**
19
+ * Turns the npm half on at runtime.
20
+ *
21
+ * Applied by wrapping rather than by a flag on the core service: "is this a Node repository" is not
22
+ * a question rman's core should be able to ask, and a `nodejs: true` option there would be exactly
23
+ * that question wearing a different hat. The wrapper translates `packageManager` into the `envinfo`
24
+ * categories the core knows nothing about.
25
+ */
26
+ export declare function augmentSystemInfo(): void;
@@ -0,0 +1,79 @@
1
+ import { SystemInfo as OrgSystemInfo } from 'rman';
2
+ /**
3
+ * Adds the npm half of `SystemInfo` to rman's own types.
4
+ *
5
+ * The core's `Options` carries nothing language-specific on purpose, so `packageManager` is not a
6
+ * setting it hid behind a flag - it is a setting it does not have. Declaration merging is what lets
7
+ * this package add it as if it had always been there, `SystemInfo.PackageManager` included, without
8
+ * the core naming npm anywhere.
9
+ *
10
+ * The union itself is `CiService.PackageManager`, not a second copy: `ci` and `publish` already
11
+ * shell out to one of these, and two lists of the same four names drift the moment a fifth appears.
12
+ */
13
+ /** The import above is aliased precisely so this block can use the real name: declaration merging
14
+ * keys off the module specifier, not off any local binding, so `namespace SystemInfo` here *is*
15
+ * rman's - and nothing shadows anything. */
16
+ /** The `SystemInfo` types this file's wrapper relies on are declared in
17
+ * [`rman.augmentation.ts`](rman.augmentation.ts), with the rest of this plugin's - one
18
+ * `declare module` block per package, or the others stop applying. */
19
+ /**
20
+ * Turns the npm half on at runtime.
21
+ *
22
+ * Applied by wrapping rather than by a flag on the core service: "is this a Node repository" is not
23
+ * a question rman's core should be able to ask, and a `nodejs: true` option there would be exactly
24
+ * that question wearing a different hat. The wrapper translates `packageManager` into the `envinfo`
25
+ * categories the core knows nothing about.
26
+ */
27
+ export function augmentSystemInfo() {
28
+ const base = OrgSystemInfo.getSystemInfo;
29
+ /**
30
+ * Idempotent, and marked on **the function** rather than in a module-level flag: the flag made
31
+ * re-application impossible once anything had replaced `getSystemInfo` - a test substituting a
32
+ * stub, or a second augmentation layering on - so whether the wrapper was actually installed came
33
+ * down to import order. Asking the current implementation whether it is already wrapped cannot go
34
+ * stale that way.
35
+ */
36
+ if (base[AUGMENTED])
37
+ return;
38
+ /** A namespace's exported function is a property of a plain object at runtime, so this is an
39
+ * ordinary assignment - the ESM *binding* is immutable, the object it points at is not. */
40
+ const wrapped = options => {
41
+ /** An explicit argument first, then the repository's own config, then npm. The config step is
42
+ * why the core declares `Options.repository`: `rman info` passes only that, so without it a
43
+ * pnpm repository would silently be reported as an npm one. */
44
+ const packageManager = options?.packageManager ?? asPackageManager(options?.repository?.config?.packageManager) ?? 'npm';
45
+ return base({
46
+ ...options,
47
+ envinfo: {
48
+ /** Replaces the core's `Binaries: ['Node']` - `envinfo` categories merge by key, not by
49
+ * concatenation, so the whole list has to be restated. */
50
+ Binaries: ['Node', PACKAGE_MANAGER_BINARY[packageManager]],
51
+ /** This package's own version alongside rman's - when `info` is being read to work out why
52
+ * a command behaved oddly, which plugin version is installed is half the answer. */
53
+ npmPackages: ['rman', 'rman-node', 'typescript'],
54
+ npmGlobalPackages: ['typescript'],
55
+ ...options?.envinfo,
56
+ },
57
+ });
58
+ };
59
+ wrapped[AUGMENTED] = true;
60
+ OrgSystemInfo.getSystemInfo = wrapped;
61
+ }
62
+ /** `.rmanrc "packageManager"` value -> the `Binaries` key `envinfo` recognizes for it (`npm`/
63
+ * `pnpm`/`bun` are lowercase, `Yarn` isn't - envinfo's own naming, not ours). */
64
+ const PACKAGE_MANAGER_BINARY = {
65
+ npm: 'npm',
66
+ yarn: 'Yarn',
67
+ pnpm: 'pnpm',
68
+ bun: 'bun',
69
+ };
70
+ /** A config value is whatever the file said, so an unrecognized one is ignored rather than used to
71
+ * index `PACKAGE_MANAGER_BINARY` and produce `Binaries: ['Node', undefined]`. */
72
+ function asPackageManager(value) {
73
+ return typeof value === 'string' && value in PACKAGE_MANAGER_BINARY
74
+ ? value
75
+ : undefined;
76
+ }
77
+ /** Marks an implementation as already wrapped. A symbol rather than a property name so it cannot
78
+ * collide with anything `envinfo` or a future core option carries. */
79
+ const AUGMENTED = Symbol.for('rman-node.systemInfo.augmented');
@@ -0,0 +1,18 @@
1
+ import { Workspace } from 'rman';
2
+ /**
3
+ * Finds an npm workspace's packages: the `workspaces` globs in the root `package.json`, keeping
4
+ * every matched directory that holds a `package.json` of its own.
5
+ *
6
+ * This is the deepest thing that moved out of the core, and the one that most needed to. "A package
7
+ * is a directory with a `package.json`" and "the repository lists its members in `workspaces`" are
8
+ * npm's ideas - a Cargo workspace, a `go.work` and a `pyproject.toml` each say the same thing
9
+ * differently, and a core that hardcoded one of them could never read the others.
10
+ *
11
+ * Returns `undefined` for a root with no `workspaces` array, which is how a single-package
12
+ * repository still works: the core treats "no provider recognized this" as "the repository is the
13
+ * package". Note that this makes no upward walk of its own - the root was already decided, without
14
+ * any ecosystem's help, before this is called.
15
+ */
16
+ export declare const npmWorkspace: Workspace.Provider;
17
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
18
+ export declare function augmentWorkspace(): void;
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import glob from 'fast-glob';
4
+ import { Workspace } from 'rman';
5
+ /**
6
+ * Finds an npm workspace's packages: the `workspaces` globs in the root `package.json`, keeping
7
+ * every matched directory that holds a `package.json` of its own.
8
+ *
9
+ * This is the deepest thing that moved out of the core, and the one that most needed to. "A package
10
+ * is a directory with a `package.json`" and "the repository lists its members in `workspaces`" are
11
+ * npm's ideas - a Cargo workspace, a `go.work` and a `pyproject.toml` each say the same thing
12
+ * differently, and a core that hardcoded one of them could never read the others.
13
+ *
14
+ * Returns `undefined` for a root with no `workspaces` array, which is how a single-package
15
+ * repository still works: the core treats "no provider recognized this" as "the repository is the
16
+ * package". Note that this makes no upward walk of its own - the root was already decided, without
17
+ * any ecosystem's help, before this is called.
18
+ */
19
+ export const npmWorkspace = (root) => {
20
+ const manifest = path.join(root, 'package.json');
21
+ if (!fs.existsSync(manifest))
22
+ return undefined;
23
+ let patterns;
24
+ try {
25
+ patterns = JSON.parse(fs.readFileSync(manifest, 'utf-8'))?.workspaces;
26
+ }
27
+ catch {
28
+ /** A malformed root `package.json` is not this provider's error to report - `Package` will do
29
+ * it with the file in hand when something actually reads the manifest. */
30
+ return undefined;
31
+ }
32
+ if (!Array.isArray(patterns))
33
+ return undefined;
34
+ const packageDirs = [];
35
+ for (const pattern of patterns) {
36
+ if (typeof pattern !== 'string')
37
+ continue;
38
+ const dirs = glob.sync(pattern, { cwd: root, absolute: true, deep: 0, onlyDirectories: true });
39
+ for (const dir of dirs) {
40
+ if (fs.existsSync(path.join(dir, 'package.json')))
41
+ packageDirs.push(dir);
42
+ }
43
+ }
44
+ return { root, packageDirs };
45
+ };
46
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
47
+ export function augmentWorkspace() {
48
+ Workspace.addProvider(npmWorkspace);
49
+ }
@@ -0,0 +1,9 @@
1
+ import { type CustomCommand } from 'rman';
2
+ /**
3
+ * `rman ci` - contributed by the `rman-node` plugin rather than built into rman.
4
+ *
5
+ * A plugin's command is a `CustomCommand`, so the repository arrives through `context` instead of
6
+ * being captured when the command is registered - the same shape a repository's own `.rman/*.mjs`
7
+ * command has, and the reason both can be registered through one code path.
8
+ */
9
+ export declare const command: CustomCommand;