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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/augmentation/manifest.augmentation.d.ts +17 -0
- package/augmentation/manifest.augmentation.js +129 -0
- package/augmentation/rman.augmentation.d.ts +45 -0
- package/augmentation/rman.augmentation.js +1 -0
- package/augmentation/run.augmentation.d.ts +15 -0
- package/augmentation/run.augmentation.js +61 -0
- package/augmentation/system-info.augmentation.d.ts +26 -0
- package/augmentation/system-info.augmentation.js +79 -0
- package/augmentation/workspace.augmentation.d.ts +18 -0
- package/augmentation/workspace.augmentation.js +49 -0
- package/commands/ci.command.d.ts +9 -0
- package/commands/ci.command.js +35 -0
- package/commands/clean.command.d.ts +6 -0
- package/commands/clean.command.js +32 -0
- package/commands/publish.command.d.ts +9 -0
- package/commands/publish.command.js +228 -0
- package/index.d.ts +31 -0
- package/index.js +100 -0
- package/interfaces/rman-config.interface.d.ts +80 -0
- package/interfaces/rman-config.interface.js +7 -0
- package/package.json +54 -0
- package/services/ci.service.d.ts +38 -0
- package/services/ci.service.js +201 -0
- package/services/clean.service.d.ts +51 -0
- package/services/clean.service.js +235 -0
- package/services/publish.service.d.ts +77 -0
- package/services/publish.service.js +258 -0
- package/services/version-plan.service.d.ts +50 -0
- package/services/version-plan.service.js +67 -0
- package/utils/npm-run-path.d.ts +21 -0
- package/utils/npm-run-path.js +36 -0
- package/utils/npm-view.d.ts +15 -0
- package/utils/npm-view.js +28 -0
- package/utils/workspace-range.d.ts +26 -0
- 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;
|