@opndev/rzilla 0.0.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/Changes +5 -0
- package/LICENSE +18 -0
- package/README.md +279 -0
- package/lib/builddir.mjs +53 -0
- package/lib/changes.mjs +85 -0
- package/lib/cmd/build.mjs +71 -0
- package/lib/cmd/clean.mjs +18 -0
- package/lib/cmd/pkg.mjs +114 -0
- package/lib/cmd/prereqs.mjs +47 -0
- package/lib/cmd/release.mjs +285 -0
- package/lib/config.mjs +12 -0
- package/lib/plugins/autoprereqs.mjs +140 -0
- package/lib/plugins/exports.mjs +36 -0
- package/lib/plugins/gather.mjs +36 -0
- package/lib/plugins/prereqs.mjs +45 -0
- package/lib/plugins/repository.mjs +141 -0
- package/lib/populate.mjs +69 -0
- package/lib/util.mjs +44 -0
- package/lib/version.mjs +64 -0
- package/package.json +50 -0
package/Changes
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Wesley Schwengle <wesleys@opperschaap.net>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# @opndev/rzilla
|
|
8
|
+
|
|
9
|
+
Release zilla for npm packages.
|
|
10
|
+
|
|
11
|
+
`rzil` lets you replace manual `package.json` maintenance with a
|
|
12
|
+
declarative `dist.toml` file.\
|
|
13
|
+
Inspired by Dist::Zilla, but for npm.
|
|
14
|
+
|
|
15
|
+
## Philosophy
|
|
16
|
+
|
|
17
|
+
- `dist.toml` is the source of truth.
|
|
18
|
+
- `package.json` is a generated artifact.
|
|
19
|
+
- Strict where ambiguity is dangerous.
|
|
20
|
+
- Ergonomic where it does not matter.
|
|
21
|
+
- No automatic pushing.
|
|
22
|
+
- Deterministic releases.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
The following commands are listed, but maybe incomplete. Please run the actual
|
|
27
|
+
script for an update to date listing.
|
|
28
|
+
|
|
29
|
+
``` bash
|
|
30
|
+
rzil pkg # Generate package.json from dist.toml
|
|
31
|
+
rzil release # Run release workflow
|
|
32
|
+
rzil test # npm test
|
|
33
|
+
rzil build # Run build workflow
|
|
34
|
+
rzil clean # Clean the .build dir
|
|
35
|
+
rzil prereqs # List all the dependencies
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
# dist.toml Example
|
|
39
|
+
|
|
40
|
+
``` toml
|
|
41
|
+
name = "@opndev/rzilla"
|
|
42
|
+
description = "Release zilla for npm packages"
|
|
43
|
+
type = "module"
|
|
44
|
+
keywords = ["release","npm","changes","git"]
|
|
45
|
+
|
|
46
|
+
[license]
|
|
47
|
+
spdx = "MIT"
|
|
48
|
+
file = "LICENSES/MIT.txt"
|
|
49
|
+
|
|
50
|
+
[author]
|
|
51
|
+
name = "Your Name"
|
|
52
|
+
email = "you@example.com"
|
|
53
|
+
|
|
54
|
+
[repository]
|
|
55
|
+
remote = "origin"
|
|
56
|
+
|
|
57
|
+
[prereqs]
|
|
58
|
+
node = ">= 18 < 22"
|
|
59
|
+
npm = ">= 9"
|
|
60
|
+
lodash = "^4.17.0"
|
|
61
|
+
|
|
62
|
+
[prereqs.dev]
|
|
63
|
+
tap = "latest"
|
|
64
|
+
jsdoc = "0"
|
|
65
|
+
|
|
66
|
+
[prereqs.peer]
|
|
67
|
+
left-pad = "latest"
|
|
68
|
+
|
|
69
|
+
[gather]
|
|
70
|
+
main = "lib/index.mjs"
|
|
71
|
+
bin = "bin"
|
|
72
|
+
files = "lib/**/*.mjs"
|
|
73
|
+
include = ["README.md"]
|
|
74
|
+
|
|
75
|
+
[exports]
|
|
76
|
+
__DOT__ = "lib/index.mjs"
|
|
77
|
+
foo = "lib/foo.mjs"
|
|
78
|
+
|
|
79
|
+
[release]
|
|
80
|
+
changes = "Changes"
|
|
81
|
+
tagPrefix = "v"
|
|
82
|
+
bump = "patch"
|
|
83
|
+
access = "public"
|
|
84
|
+
|
|
85
|
+
[release.preflight]
|
|
86
|
+
dirty = "dist.toml"
|
|
87
|
+
airplane = false
|
|
88
|
+
|
|
89
|
+
[release.after]
|
|
90
|
+
commit = ["Changes", "dist.toml", "package.json"]
|
|
91
|
+
bump = true
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
# Key Concepts
|
|
95
|
+
|
|
96
|
+
## package.json is generated
|
|
97
|
+
|
|
98
|
+
Run:
|
|
99
|
+
|
|
100
|
+
``` bash
|
|
101
|
+
rzil pkg
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This creates:
|
|
105
|
+
|
|
106
|
+
- `package.json`
|
|
107
|
+
- `bin` entries from `gather.bin`
|
|
108
|
+
- `exports` map
|
|
109
|
+
- dependencies from `[prereqs]`
|
|
110
|
+
- engines from `node` and `npm`
|
|
111
|
+
- repository + homepage derived from git remote
|
|
112
|
+
|
|
113
|
+
Do not edit `package.json` manually.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## Prereqs
|
|
117
|
+
|
|
118
|
+
Prereqs or dependencies can be set via `[prereqs]` and friends:
|
|
119
|
+
|
|
120
|
+
``` toml
|
|
121
|
+
[prereqs]
|
|
122
|
+
node = ">= 18 < 22"
|
|
123
|
+
npm = ">= 9"
|
|
124
|
+
foo = "^1.2.3"
|
|
125
|
+
bar = 0
|
|
126
|
+
baz = "latest"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Mapping:
|
|
130
|
+
|
|
131
|
+
- `node` → `engines.node`
|
|
132
|
+
- `npm` → `engines.npm`
|
|
133
|
+
- others → `dependencies`
|
|
134
|
+
- `0` → `"*"`
|
|
135
|
+
- Spaces in ranges are normalized (`"> 18 < 22"` → `">18 <22"`)
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[prereqs.test]
|
|
139
|
+
tap = "latest"
|
|
140
|
+
|
|
141
|
+
[prereqs.peer]
|
|
142
|
+
left-pad = "latest"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Autoprereqs
|
|
146
|
+
|
|
147
|
+
When enabled, rzil scans your source tree and fills in missing dependencies:
|
|
148
|
+
|
|
149
|
+
``` toml
|
|
150
|
+
[autoprereqs]
|
|
151
|
+
# enabled when the table exists
|
|
152
|
+
# enabled = true
|
|
153
|
+
ignore = ["node:fs"]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- Runtime imports (uses the `[gather]` paths/globs) are added to
|
|
157
|
+
`dependencies`.
|
|
158
|
+
- Test imports (from `t/`, `test/`, `tests/`, `__tests__/`) are added to
|
|
159
|
+
`devDependencies`.
|
|
160
|
+
- Inferred versions default to `"latest"`.
|
|
161
|
+
- Explicit entries in `[prereqs]` always win.
|
|
162
|
+
- Relative imports and Node builtins are ignored.
|
|
163
|
+
|
|
164
|
+
## Gather
|
|
165
|
+
|
|
166
|
+
``` toml
|
|
167
|
+
[gather]
|
|
168
|
+
main = "lib/index.mjs"
|
|
169
|
+
bin = ["bin", "cli"]
|
|
170
|
+
files = "lib/**/*.mjs"
|
|
171
|
+
include = ["README.md"]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `main` defines the root export `"."`
|
|
175
|
+
- `bin` auto-discovers CLI files
|
|
176
|
+
- `files` and `include` populate `package.json.files`
|
|
177
|
+
- `bin` supports string or array
|
|
178
|
+
- You don't need to add your license file, it is taken from `license.file`.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
``` toml
|
|
184
|
+
[license]
|
|
185
|
+
spdx = "MIT"
|
|
186
|
+
file = "LICENSES/MIT.txt"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- `spdx` becomes `package.json.license`.
|
|
190
|
+
- `file` is written as `LICENSE` in the build artifact.
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## Exports
|
|
194
|
+
|
|
195
|
+
``` toml
|
|
196
|
+
[exports]
|
|
197
|
+
__DOT__ = "lib/index.mjs"
|
|
198
|
+
foo = "lib/foo.mjs"
|
|
199
|
+
|
|
200
|
+
[exports.deny]
|
|
201
|
+
testing = true
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Rules:
|
|
205
|
+
|
|
206
|
+
- `__DOT__` → `"."`
|
|
207
|
+
- other keys → `"./key"`
|
|
208
|
+
- `exports.deny` removes subpaths
|
|
209
|
+
- `exports.deny.__DOT__` is forbidden
|
|
210
|
+
|
|
211
|
+
If `[exports]` exists, you fully control the export surface.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## Repository
|
|
215
|
+
|
|
216
|
+
``` toml
|
|
217
|
+
[repository]
|
|
218
|
+
remote = "origin"
|
|
219
|
+
provider = "github" # optional
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
rzil:
|
|
223
|
+
|
|
224
|
+
- reads `git remote`
|
|
225
|
+
- derives `repository.url`
|
|
226
|
+
- derives `repository.homepage`
|
|
227
|
+
- derives `bugs.url` if `[bugtracker]` exists
|
|
228
|
+
- supports github, gitlab, codeberg, bitbucket
|
|
229
|
+
- supports private hosts with `provider`
|
|
230
|
+
|
|
231
|
+
rzil never pushes automatically.
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
## Release Workflow
|
|
235
|
+
|
|
236
|
+
``` bash
|
|
237
|
+
rzil release
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Steps:
|
|
241
|
+
|
|
242
|
+
1. Check `{{ NEXT }}` in Changes has entries
|
|
243
|
+
2. Run tests
|
|
244
|
+
3. Check git dirty state (with allowlist)
|
|
245
|
+
4. Create a build directory (`.build/<id>/` and `.build/current`)
|
|
246
|
+
5. Copy only publishable files into the build directory
|
|
247
|
+
6. Generate `package.json` (and other build artifacts) into the build
|
|
248
|
+
directory
|
|
249
|
+
7. Finalize Changes
|
|
250
|
+
8. Commit release
|
|
251
|
+
9. Tag release
|
|
252
|
+
10. `npm publish` from `.build/current` (unless airplane mode)
|
|
253
|
+
11. Restore `{{ NEXT }}`
|
|
254
|
+
12. Bump version (optional)
|
|
255
|
+
13. Commit post-release files
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
You push manually.
|
|
259
|
+
|
|
260
|
+
Airplane mode disables network actions.
|
|
261
|
+
|
|
262
|
+
## Status
|
|
263
|
+
|
|
264
|
+
Early-stage but functional.
|
|
265
|
+
|
|
266
|
+
## Developer notes
|
|
267
|
+
|
|
268
|
+
### Semver
|
|
269
|
+
|
|
270
|
+
This package does not adhere to semver. It's just a version number.
|
|
271
|
+
|
|
272
|
+
### Lock files
|
|
273
|
+
|
|
274
|
+
Ignored. Keep your deps minimal and I'll promise to not break your code.
|
|
275
|
+
|
|
276
|
+
### Code of Conduct
|
|
277
|
+
|
|
278
|
+
Be human.
|
|
279
|
+
|
package/lib/builddir.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
async function mkdirp(p) {
|
|
10
|
+
await fs.mkdir(p, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function rmForce(p) {
|
|
14
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function randId() {
|
|
18
|
+
// short, readable, collision-resistant enough for builds
|
|
19
|
+
return crypto.randomBytes(8).toString("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates .build/<id>/ and updates .build/current -> <id>
|
|
24
|
+
* Returns { buildRoot, buildDir, currentPath }
|
|
25
|
+
*/
|
|
26
|
+
export async function createBuildDir({ buildRoot = ".build" } = {}) {
|
|
27
|
+
const id = randId();
|
|
28
|
+
const buildDir = path.join(buildRoot, id);
|
|
29
|
+
const currentPath = path.join(buildRoot, "current");
|
|
30
|
+
|
|
31
|
+
await mkdirp(buildDir);
|
|
32
|
+
|
|
33
|
+
// Replace current pointer atomically-ish:
|
|
34
|
+
// Create a temp symlink then rename it into place.
|
|
35
|
+
const tmpLink = path.join(buildRoot, `.current-tmp-${id}`);
|
|
36
|
+
|
|
37
|
+
// Ensure buildRoot exists
|
|
38
|
+
await mkdirp(buildRoot);
|
|
39
|
+
|
|
40
|
+
// Remove any stale temp link
|
|
41
|
+
await rmForce(tmpLink);
|
|
42
|
+
|
|
43
|
+
// Create symlink pointing to the *id directory*, relative for readability
|
|
44
|
+
// current -> <id>
|
|
45
|
+
await fs.symlink(id, tmpLink, "dir");
|
|
46
|
+
|
|
47
|
+
// Remove old current, then move tmp into place
|
|
48
|
+
await rmForce(currentPath);
|
|
49
|
+
await fs.rename(tmpLink, currentPath);
|
|
50
|
+
|
|
51
|
+
return { buildRoot, buildDir, currentPath, id };
|
|
52
|
+
}
|
|
53
|
+
|
package/lib/changes.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
export async function readChanges(path) {
|
|
8
|
+
return fs.readFile(path, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function writeChanges(path, text) {
|
|
12
|
+
await fs.writeFile(path, text, "utf8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatUtcStamp(date = new Date()) {
|
|
16
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
17
|
+
const y = date.getUTCFullYear();
|
|
18
|
+
const m = pad(date.getUTCMonth() + 1);
|
|
19
|
+
const d = pad(date.getUTCDate());
|
|
20
|
+
const hh = pad(date.getUTCHours());
|
|
21
|
+
const mm = pad(date.getUTCMinutes());
|
|
22
|
+
const ss = pad(date.getUTCSeconds());
|
|
23
|
+
return `${y}-${m}-${d} ${hh}:${mm}:${ss}Z`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function extractNextSection(changesText) {
|
|
27
|
+
const marker = "{{ NEXT }}";
|
|
28
|
+
const idx = changesText.indexOf(marker);
|
|
29
|
+
if (idx === -1) throw new Error(`Could not find ${marker} in Changes`);
|
|
30
|
+
|
|
31
|
+
const after = changesText.slice(idx + marker.length);
|
|
32
|
+
|
|
33
|
+
// End NEXT section at the first blank line before a version header like:
|
|
34
|
+
// 0.0.8 2024-06-04 ...
|
|
35
|
+
const match = after.match(/\n\s*\n(?=\d+\.\d+\.\d+\s+)/);
|
|
36
|
+
const endRel = match ? match.index + match[0].length : after.length;
|
|
37
|
+
|
|
38
|
+
const nextBody = after.slice(0, endRel);
|
|
39
|
+
return { nextBody };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function nextHasEntries(changesText) {
|
|
43
|
+
const { nextBody } = extractNextSection(changesText);
|
|
44
|
+
const lines = nextBody.split("\n").map((l) => l.trim());
|
|
45
|
+
return lines.some((l) => l.startsWith("* ") || l.startsWith("- "));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function finalizeNextToVersion(changesText, version) {
|
|
49
|
+
const marker = "{{ NEXT }}";
|
|
50
|
+
const stamp = formatUtcStamp();
|
|
51
|
+
const replacement = `${version} ${stamp}`;
|
|
52
|
+
|
|
53
|
+
if (!changesText.includes(marker)) {
|
|
54
|
+
throw new Error(`Could not find ${marker} in Changes`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Replace only the first occurrence
|
|
58
|
+
return changesText.replace(marker, replacement);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ensureNextInserted(changesText) {
|
|
62
|
+
const marker = "{{ NEXT }}";
|
|
63
|
+
if (changesText.includes(marker)) return changesText;
|
|
64
|
+
|
|
65
|
+
const lines = changesText.split("\n");
|
|
66
|
+
|
|
67
|
+
// Insert after the first blank line (usually after "Revision history for ...")
|
|
68
|
+
let insertAt = 1;
|
|
69
|
+
for (let i = 0; i < Math.min(lines.length, 20); i++) {
|
|
70
|
+
if (lines[i].trim() === "" && i > 0) {
|
|
71
|
+
insertAt = i + 1;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const block = [
|
|
77
|
+
"{{ NEXT }}",
|
|
78
|
+
"",
|
|
79
|
+
" * ",
|
|
80
|
+
"",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
lines.splice(insertAt, 0, ...block);
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
import { readDistToml } from "../config.mjs";
|
|
9
|
+
import { asArray } from "@opndev/util";
|
|
10
|
+
|
|
11
|
+
import { createBuildDir } from "../builddir.mjs";
|
|
12
|
+
import { populateBuildDir } from "../populate.mjs";
|
|
13
|
+
import { runPkg } from "./pkg.mjs";
|
|
14
|
+
|
|
15
|
+
async function writeBuildLicense({ buildDir, license }) {
|
|
16
|
+
// dist.toml:
|
|
17
|
+
// [license]
|
|
18
|
+
// spdx = "MIT"
|
|
19
|
+
// file = "LICENSES/MIT.txt"
|
|
20
|
+
if (!license?.file) return;
|
|
21
|
+
|
|
22
|
+
const text = await fs.readFile(license.file, "utf8");
|
|
23
|
+
await fs.writeFile(path.join(buildDir, "LICENSE"), text, "utf8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function copyAfterBuild({ buildDir, copyList }) {
|
|
27
|
+
for (const rel of copyList) {
|
|
28
|
+
const src = path.join(buildDir, rel);
|
|
29
|
+
const dst = path.join(process.cwd(), rel);
|
|
30
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
31
|
+
await fs.copyFile(src, dst);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runBuild() {
|
|
36
|
+
const { cfg } = await readDistToml("dist.toml");
|
|
37
|
+
|
|
38
|
+
// 1) Create build dir + .build/current
|
|
39
|
+
const { buildDir, currentPath } = await createBuildDir({ buildRoot: ".build" });
|
|
40
|
+
|
|
41
|
+
// 2) Populate build dir with publishable files only
|
|
42
|
+
{
|
|
43
|
+
const g = cfg.gather ?? {};
|
|
44
|
+
const files = asArray(g.files ?? "lib/**/*.{mjs,cjs,js}");
|
|
45
|
+
const include = asArray(g.include ?? []);
|
|
46
|
+
const bin = asArray(g.bin ?? []);
|
|
47
|
+
await populateBuildDir({
|
|
48
|
+
buildDir,
|
|
49
|
+
files,
|
|
50
|
+
include,
|
|
51
|
+
binDirs: bin, // NOTE: your populateBuildDir should treat these as globs if that's your gather model
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3) Generate package.json into build dir (emitFiles=false because build dir is already pruned)
|
|
56
|
+
await runPkg({ outDir: buildDir, emitFiles: false });
|
|
57
|
+
|
|
58
|
+
// 4) Materialize LICENSE into build dir
|
|
59
|
+
await writeBuildLicense({ buildDir, license: cfg.license });
|
|
60
|
+
|
|
61
|
+
// 5) build.after.copy back into repo root (optional)
|
|
62
|
+
{
|
|
63
|
+
const copyList = asArray(cfg.build?.after?.copy ?? []);
|
|
64
|
+
if (copyList.length) {
|
|
65
|
+
await copyAfterBuild({ buildDir, copyList });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Built distribution in ${currentPath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export async function runClean() {
|
|
9
|
+
const buildDir = path.join(process.cwd(), ".build");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
13
|
+
console.log("Removed .build directory.");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw new Error(`Failed to clean .build: ${err.message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
package/lib/cmd/pkg.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { readDistToml } from "../config.mjs";
|
|
7
|
+
import { uniq } from "@opndev/util";
|
|
8
|
+
import { deriveRepoAndBugs } from "../plugins/repository.mjs";
|
|
9
|
+
import { applyPrereqs } from "../plugins/prereqs.mjs";
|
|
10
|
+
import { buildExports } from "../plugins/exports.mjs";
|
|
11
|
+
import { gatherConfig, discoverBins, buildFilesList } from "../plugins/gather.mjs";
|
|
12
|
+
import { applyAutoPrereqs } from "../plugins/autoprereqs.mjs";
|
|
13
|
+
import { readVersion } from "../version.mjs";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async function basePkgFromCfg(cfg) {
|
|
17
|
+
const version = await readVersion(cfg);
|
|
18
|
+
const pkg = {
|
|
19
|
+
name: cfg.name,
|
|
20
|
+
version: version,
|
|
21
|
+
description: cfg.description,
|
|
22
|
+
type: cfg.type ?? "module",
|
|
23
|
+
keywords: cfg.keywords,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
pkg.sideEffects = cfg.sideEffects ?? false;
|
|
28
|
+
|
|
29
|
+
if (typeof pkg.sideEffects !== "boolean")
|
|
30
|
+
throw new Error(`sideEffects must be boolean (true/false)`);
|
|
31
|
+
|
|
32
|
+
if (cfg.private != null && typeof cfg.private !== "boolean") {
|
|
33
|
+
throw new Error(`private must be boolean (true/false), got ${typeof cfg.private}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (cfg.author) pkg.author = cfg.author;
|
|
37
|
+
|
|
38
|
+
if (cfg.license?.spdx) {
|
|
39
|
+
pkg.license = cfg.license.spdx;
|
|
40
|
+
if (cfg.private == null) cfg.private = false;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
pkg.license = "UNLICENSED";
|
|
44
|
+
if (cfg.private == null) cfg.private = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (cfg.private) pkg.private = cfg.private;
|
|
48
|
+
|
|
49
|
+
if (cfg.scripts) pkg.scripts = cfg.scripts;
|
|
50
|
+
|
|
51
|
+
return pkg;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sortKeysDeep(obj) {
|
|
55
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return obj;
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const k of Object.keys(obj).sort()) out[k] = sortKeysDeep(obj[k]);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runPkg({ outDir = ".", emitFiles = true, msg = true } = {}) {
|
|
62
|
+
const { cfg } = await readDistToml("dist.toml");
|
|
63
|
+
|
|
64
|
+
const pkg = await basePkgFromCfg(cfg);
|
|
65
|
+
|
|
66
|
+
const g = gatherConfig(cfg);
|
|
67
|
+
const { bin } = await discoverBins(g.binGlob);
|
|
68
|
+
if (Object.keys(bin).length) pkg.bin = bin;
|
|
69
|
+
|
|
70
|
+
if (emitFiles) {
|
|
71
|
+
const files = buildFilesList({
|
|
72
|
+
files: g.files,
|
|
73
|
+
include: g.include,
|
|
74
|
+
binGlob: g.binGlob,
|
|
75
|
+
});
|
|
76
|
+
if (files.length) pkg.files = files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ex = buildExports(cfg);
|
|
80
|
+
if (ex) pkg.exports = ex;
|
|
81
|
+
|
|
82
|
+
applyPrereqs(cfg, pkg);
|
|
83
|
+
|
|
84
|
+
const ap = cfg.autoprereqs;
|
|
85
|
+
if (ap && ap.enabled !== false) {
|
|
86
|
+
await applyAutoPrereqs(cfg, pkg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (cfg.repository) {
|
|
90
|
+
const info = await deriveRepoAndBugs({
|
|
91
|
+
repository: cfg.repository,
|
|
92
|
+
bugtracker: cfg.bugtracker ?? null,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pkg.repository = { type: cfg.repository.type ?? "git", url: `git+${info.remoteUrl ?? cfg.repository.url}` };
|
|
96
|
+
if (info.web) pkg.homepage = info.web;
|
|
97
|
+
|
|
98
|
+
if (cfg.bugtracker && info.bugsUrl) pkg.bugs = { url: info.bugsUrl };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fall back to cfg.bugtracker.url where needed
|
|
102
|
+
if (cfg.bugtracker && cfg.bugtracker.url) pkg.bugs = { url: cfg.bugtracker.url };
|
|
103
|
+
|
|
104
|
+
// cleanup empties
|
|
105
|
+
if (pkg.keywords && Array.isArray(pkg.keywords)) pkg.keywords = uniq(pkg.keywords);
|
|
106
|
+
|
|
107
|
+
const finalPkg = sortKeysDeep(pkg);
|
|
108
|
+
|
|
109
|
+
const outPath = `${outDir}/package.json`;
|
|
110
|
+
await fs.writeFile(outPath, JSON.stringify(finalPkg, null, 2) + "\n", "utf8");
|
|
111
|
+
|
|
112
|
+
if (msg) console.log("Generated package.json from dist.toml");
|
|
113
|
+
}
|
|
114
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readDistToml } from "../config.mjs";
|
|
2
|
+
import { applyPrereqs } from "../plugins/prereqs.mjs";
|
|
3
|
+
import { applyAutoPrereqs } from "../plugins/autoprereqs.mjs";
|
|
4
|
+
|
|
5
|
+
function sortedEntries(obj) {
|
|
6
|
+
return Object.entries(obj ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function printSection(title, obj) {
|
|
10
|
+
const entries = sortedEntries(obj);
|
|
11
|
+
if (!entries.length) return;
|
|
12
|
+
console.log(`${title}:`);
|
|
13
|
+
for (const [k, v] of entries) console.log(` ${k} ${v}`);
|
|
14
|
+
console.log("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runPrereqs(opts = {}) {
|
|
18
|
+
const { cfg } = await readDistToml("dist.toml");
|
|
19
|
+
|
|
20
|
+
// Build a pkg-like object using the same merge logic as pkg generation.
|
|
21
|
+
const pkg = {};
|
|
22
|
+
applyPrereqs(cfg, pkg);
|
|
23
|
+
|
|
24
|
+
// autoprereqs is opt-in by table presence, and enabled unless enabled=false
|
|
25
|
+
const ap = cfg.autoprereqs;
|
|
26
|
+
if (ap && ap.enabled !== false) {
|
|
27
|
+
await applyAutoPrereqs(cfg, pkg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const out = {
|
|
31
|
+
engines: pkg.engines ?? {},
|
|
32
|
+
dependencies: pkg.dependencies ?? {},
|
|
33
|
+
devDependencies: pkg.devDependencies ?? {},
|
|
34
|
+
peerDependencies: pkg.peerDependencies ?? {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
console.log(JSON.stringify(out, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
printSection("Engines", out.engines);
|
|
43
|
+
printSection("Runtime dependencies", out.dependencies);
|
|
44
|
+
printSection("Dev dependencies", out.devDependencies);
|
|
45
|
+
printSection("Peer dependencies", out.peerDependencies);
|
|
46
|
+
}
|
|
47
|
+
|