@sboudrias/package 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -7
- package/dist/bin.js +5 -29
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lint/command.d.ts +7 -0
- package/dist/lint/command.js +28 -0
- package/dist/{lint.js → lint/index.js} +3 -3
- package/dist/normalize/command.d.ts +6 -0
- package/dist/normalize/command.js +22 -0
- package/dist/normalize/index.d.ts +13 -0
- package/dist/normalize/index.js +48 -0
- package/dist/pin/command.d.ts +7 -0
- package/dist/pin/command.js +28 -0
- package/dist/pin/index.d.ts +29 -0
- package/dist/pin/index.js +54 -0
- package/package.json +4 -3
- /package/dist/{lint.d.ts → lint/index.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -43,7 +43,17 @@ bun add @sboudrias/package --dev
|
|
|
43
43
|
</tr>
|
|
44
44
|
</table>
|
|
45
45
|
|
|
46
|
-
#
|
|
46
|
+
# Commands
|
|
47
|
+
|
|
48
|
+
| Command | Description |
|
|
49
|
+
| ----------------------------------------------------- | ------------------------------------------------------------- |
|
|
50
|
+
| [`package lint`](#package-lint) | Validate and fix package metadata. |
|
|
51
|
+
| [`package normalize`](#package-normalize) | Fix workspace manifests so they are valid for npm publishing. |
|
|
52
|
+
| [`package pin <dependency>`](#package-pin-dependency) | Pin exact versions of a workspace dependency. |
|
|
53
|
+
|
|
54
|
+
All commands operate on public workspace packages (see [Workspace Discovery](#workspace-discovery)) and write manifests in place.
|
|
55
|
+
|
|
56
|
+
## package lint
|
|
47
57
|
|
|
48
58
|
```bash
|
|
49
59
|
package lint
|
|
@@ -57,15 +67,13 @@ package lint --check
|
|
|
57
67
|
|
|
58
68
|
`package lint --check` runs the same validation without writing files. It exits non-zero when any package needs a fix or has a manual conflict.
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Valid Peer Dependencies
|
|
70
|
+
### Valid Peer Dependencies
|
|
63
71
|
|
|
64
72
|
Runtime dependencies can declare their own peer dependencies. `package lint` makes those peer requirements visible on the package that uses the runtime dependency.
|
|
65
73
|
|
|
66
74
|
It adds missing peers to `peerDependencies` and copies matching `peerDependenciesMeta` entries so optional peers stay optional.
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
### Matching engines
|
|
69
77
|
|
|
70
78
|
Packages should only advertise Node.js support that their runtime dependencies can also support.
|
|
71
79
|
|
|
@@ -73,16 +81,40 @@ Packages should only advertise Node.js support that their runtime dependencies c
|
|
|
73
81
|
|
|
74
82
|
That failure is intentional. Bumping a dependency can raise the minimum supported Node.js version and break dependants that still install the package under the previous `engines.node` range. In that case, manually narrow the package engine range or choose a compatible dependency version.
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
### Ensure package.json is exposed
|
|
77
85
|
|
|
78
86
|
Packages should expose their manifest for tools that inspect package metadata at runtime.
|
|
79
87
|
|
|
80
88
|
`package lint` ensures public packages expose `"./package.json": "./package.json"` in `exports`.
|
|
81
89
|
|
|
90
|
+
## package normalize
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
package normalize
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`package normalize` applies every manifest fixup required to make public workspace packages valid for npm publishing, in place.
|
|
97
|
+
|
|
98
|
+
Currently it removes dev dependencies declared with the `workspace:*` protocol: those specs only resolve inside the workspace and are invalid once the package is published, so they must be stripped from the manifest before it is packed. Dev dependencies with other specs are left untouched.
|
|
99
|
+
|
|
100
|
+
As new npm-validity issues are identified, their fixups join this command instead of the calling pipeline. The Inquirer.js release pipeline runs it at `prepack` time, right before packing the tarballs.
|
|
101
|
+
|
|
102
|
+
## package pin <dependency>
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
package pin <dependency>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`package pin <dependency>` rewrites semver ranges (`^1.2.3`, `~1.2.3`) of the given dependency to exact versions (`1.2.3`) across all public workspace packages, in place.
|
|
109
|
+
|
|
110
|
+
Exact specs, `workspace:` protocols, and dev dependencies are left untouched. It exits non-zero when a range cannot be resolved to a single exact version (e.g. `>=1.0.0`, `1.x`); such ranges must be fixed manually.
|
|
111
|
+
|
|
112
|
+
The Inquirer.js release pipeline uses this at `prepack` time to pin `@inquirer/type` exactly in published manifests: type-only dependencies leak into consumers' `.d.ts` compilation, so an uncontrolled range can break downstream TypeScript builds even when the runtime behavior is unchanged.
|
|
113
|
+
|
|
82
114
|
# Workspace Discovery
|
|
83
115
|
|
|
84
116
|
The CLI discovers workspaces from `package.json` `workspaces` fields and `pnpm-workspace.yaml` files.
|
|
85
117
|
|
|
86
|
-
If no workspaces are configured, the root `package.json` is
|
|
118
|
+
If no workspaces are configured, the root `package.json` is treated as a single-package project.
|
|
87
119
|
|
|
88
120
|
Private packages are ignored by default.
|
package/dist/bin.js
CHANGED
|
@@ -1,33 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Builtins, Cli
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
description: 'Validate and fix package metadata.',
|
|
8
|
-
examples: [
|
|
9
|
-
['Fix package metadata', '$0 lint'],
|
|
10
|
-
['Validate package metadata without writing files', '$0 lint --check'],
|
|
11
|
-
],
|
|
12
|
-
});
|
|
13
|
-
check = Option.Boolean('--check', false, {
|
|
14
|
-
description: 'Validate package metadata without writing files.',
|
|
15
|
-
});
|
|
16
|
-
async execute() {
|
|
17
|
-
try {
|
|
18
|
-
const result = await lintPackages({ check: this.check });
|
|
19
|
-
for (const issue of result.issues) {
|
|
20
|
-
this.context.stderr.write(`[${issue.status}] ${issue.message} (${issue.packagePath})\n`);
|
|
21
|
-
}
|
|
22
|
-
return result.hasFailures ? 1 : 0;
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
this.context.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
26
|
-
return 1;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const cli = Cli.from([LintCommand, Builtins.HelpCommand], {
|
|
2
|
+
import { Builtins, Cli } from 'clipanion';
|
|
3
|
+
import { LintCommand } from "./lint/command.js";
|
|
4
|
+
import { NormalizeCommand } from "./normalize/command.js";
|
|
5
|
+
import { PinCommand } from "./pin/command.js";
|
|
6
|
+
const cli = Cli.from([LintCommand, NormalizeCommand, PinCommand, Builtins.HelpCommand], {
|
|
31
7
|
binaryLabel: 'Package',
|
|
32
8
|
binaryName: 'package',
|
|
33
9
|
enableCapture: false,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { lintPackages } from "./index.js";
|
|
3
|
+
export class LintCommand extends Command {
|
|
4
|
+
static paths = [['lint']];
|
|
5
|
+
static usage = Command.Usage({
|
|
6
|
+
description: 'Validate and fix package metadata.',
|
|
7
|
+
examples: [
|
|
8
|
+
['Fix package metadata', '$0 lint'],
|
|
9
|
+
['Validate package metadata without writing files', '$0 lint --check'],
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
check = Option.Boolean('--check', false, {
|
|
13
|
+
description: 'Validate package metadata without writing files.',
|
|
14
|
+
});
|
|
15
|
+
async execute() {
|
|
16
|
+
try {
|
|
17
|
+
const result = await lintPackages({ check: this.check });
|
|
18
|
+
for (const issue of result.issues) {
|
|
19
|
+
this.context.stderr.write(`[${issue.status}] ${issue.message} (${issue.packagePath})\n`);
|
|
20
|
+
}
|
|
21
|
+
return result.hasFailures ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.context.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { subset, validRange } from 'semver';
|
|
3
|
-
import { applyPeerDependencyHoists, getPeerDependencyHoists, } from "
|
|
4
|
-
import { resolveDependencyPackageJson, writePackageJsonFile } from "
|
|
5
|
-
import { readWorkspaceProject } from "
|
|
3
|
+
import { applyPeerDependencyHoists, getPeerDependencyHoists, } from "../peer-dependencies.js";
|
|
4
|
+
import { resolveDependencyPackageJson, writePackageJsonFile } from "../package-json.js";
|
|
5
|
+
import { readWorkspaceProject } from "../workspaces.js";
|
|
6
6
|
function nodeEngine(pkg) {
|
|
7
7
|
const node = pkg.engines?.['node'];
|
|
8
8
|
return typeof node === 'string' ? node : undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
import { normalizeManifests } from "./index.js";
|
|
3
|
+
export class NormalizeCommand extends Command {
|
|
4
|
+
static paths = [['normalize']];
|
|
5
|
+
static usage = Command.Usage({
|
|
6
|
+
description: 'Fix workspace manifests so they are valid for npm publishing.',
|
|
7
|
+
examples: [['Fix manifests before packing tarballs', '$0 normalize']],
|
|
8
|
+
});
|
|
9
|
+
async execute() {
|
|
10
|
+
try {
|
|
11
|
+
const fixups = await normalizeManifests();
|
|
12
|
+
for (const fixup of fixups) {
|
|
13
|
+
this.context.stdout.write(`[fixed] ${fixup.name}: removed workspace dev dependency ${fixup.dependencyName}.\n`);
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
this.context.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ManifestFixup = {
|
|
2
|
+
kind: 'remove-workspace-dev-dependency';
|
|
3
|
+
packagePath: string;
|
|
4
|
+
name: string;
|
|
5
|
+
dependencyName: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Applies every manifest fixup required to make public workspace packages
|
|
9
|
+
* valid for npm publishing. Run before packing tarballs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeManifests(options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}): Promise<ManifestFixup[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writePackageJsonFile } from "../package-json.js";
|
|
3
|
+
import { readWorkspaceProject } from "../workspaces.js";
|
|
4
|
+
/**
|
|
5
|
+
* Removes dev dependencies declared with the `workspace:*` protocol. Those
|
|
6
|
+
* specs only resolve inside the workspace and are invalid once the package is
|
|
7
|
+
* published, so they must be stripped from the manifest before it is packed
|
|
8
|
+
* into the tarball. Dev dependencies with other specs are left untouched.
|
|
9
|
+
*/
|
|
10
|
+
function fixWorkspaceDevDependencies(pkg) {
|
|
11
|
+
const { packageJson, packagePath } = pkg;
|
|
12
|
+
if (packageJson.private === true)
|
|
13
|
+
return [];
|
|
14
|
+
const name = packageJson.name ?? packagePath;
|
|
15
|
+
const devDependencies = packageJson.devDependencies;
|
|
16
|
+
if (devDependencies == null)
|
|
17
|
+
return [];
|
|
18
|
+
const entries = Object.entries(devDependencies);
|
|
19
|
+
const kept = entries.filter(([, spec]) => spec !== 'workspace:*');
|
|
20
|
+
if (kept.length === entries.length)
|
|
21
|
+
return [];
|
|
22
|
+
packageJson.devDependencies = Object.fromEntries(kept);
|
|
23
|
+
return entries.flatMap(([dependencyName, spec]) => spec === 'workspace:*'
|
|
24
|
+
? [
|
|
25
|
+
{
|
|
26
|
+
kind: 'remove-workspace-dev-dependency',
|
|
27
|
+
packagePath,
|
|
28
|
+
name,
|
|
29
|
+
dependencyName,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
: []);
|
|
33
|
+
}
|
|
34
|
+
const FIXUPS = [fixWorkspaceDevDependencies];
|
|
35
|
+
/**
|
|
36
|
+
* Applies every manifest fixup required to make public workspace packages
|
|
37
|
+
* valid for npm publishing. Run before packing tarballs.
|
|
38
|
+
*/
|
|
39
|
+
export async function normalizeManifests(options = {}) {
|
|
40
|
+
const cwd = options.cwd ?? process.cwd();
|
|
41
|
+
const { packages } = await readWorkspaceProject(cwd);
|
|
42
|
+
const fixups = packages.flatMap((pkg) => FIXUPS.flatMap((fixup) => fixup(pkg)));
|
|
43
|
+
const changedPackagePaths = new Set(fixups.map((fixup) => fixup.packagePath));
|
|
44
|
+
await Promise.all(packages
|
|
45
|
+
.filter(({ packagePath }) => changedPackagePaths.has(packagePath))
|
|
46
|
+
.map(({ packageJson, packagePath }) => writePackageJsonFile(path.join(cwd, packagePath), packageJson)));
|
|
47
|
+
return fixups;
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { pinDependencies } from "./index.js";
|
|
3
|
+
export class PinCommand extends Command {
|
|
4
|
+
static paths = [['pin']];
|
|
5
|
+
static usage = Command.Usage({
|
|
6
|
+
description: 'Pin exact versions of workspace dependencies.',
|
|
7
|
+
examples: [['Pin a dependency across all public packages', '$0 pin @inquirer/type']],
|
|
8
|
+
});
|
|
9
|
+
dependencyName = Option.String({ required: true });
|
|
10
|
+
async execute() {
|
|
11
|
+
try {
|
|
12
|
+
const result = await pinDependencies({
|
|
13
|
+
dependencyNames: [this.dependencyName],
|
|
14
|
+
});
|
|
15
|
+
for (const pin of result.pinned) {
|
|
16
|
+
this.context.stdout.write(`[fixed] ${pin.name}: pinned ${pin.dependencyName} from "${pin.range}" to "${pin.exact}".\n`);
|
|
17
|
+
}
|
|
18
|
+
for (const unpinnable of result.unpinnable) {
|
|
19
|
+
this.context.stderr.write(`[error] ${unpinnable.name} declares ${unpinnable.dependencyName} with an unsupported range "${unpinnable.range}" (${unpinnable.packagePath})\n`);
|
|
20
|
+
}
|
|
21
|
+
return result.unpinnable.length > 0 ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.context.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type PinnedDependency = {
|
|
2
|
+
packagePath: string;
|
|
3
|
+
name: string;
|
|
4
|
+
dependencyName: string;
|
|
5
|
+
range: string;
|
|
6
|
+
exact: string;
|
|
7
|
+
};
|
|
8
|
+
export type UnpinnableDependency = {
|
|
9
|
+
packagePath: string;
|
|
10
|
+
name: string;
|
|
11
|
+
dependencyName: string;
|
|
12
|
+
range: string;
|
|
13
|
+
};
|
|
14
|
+
export type PinDependenciesResult = {
|
|
15
|
+
pinned: PinnedDependency[];
|
|
16
|
+
unpinnable: UnpinnableDependency[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Rewrites semver ranges (`^1.2.3`, `~1.2.3`) of the given dependencies to
|
|
20
|
+
* exact versions (`1.2.3`) across all public workspace packages.
|
|
21
|
+
*
|
|
22
|
+
* Exact specs and `workspace:` protocols are left untouched. Ranges that
|
|
23
|
+
* cannot be resolved to a single exact version (e.g. `>=1.0.0`, `1.x`) are
|
|
24
|
+
* reported as unpinnable and must be fixed manually.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pinDependencies(options: {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
dependencyNames: string[];
|
|
29
|
+
}): Promise<PinDependenciesResult>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { valid as validVersion } from 'semver';
|
|
3
|
+
import { writePackageJsonFile } from "../package-json.js";
|
|
4
|
+
import { readWorkspaceProject } from "../workspaces.js";
|
|
5
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies'];
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites semver ranges (`^1.2.3`, `~1.2.3`) of the given dependencies to
|
|
8
|
+
* exact versions (`1.2.3`) across all public workspace packages.
|
|
9
|
+
*
|
|
10
|
+
* Exact specs and `workspace:` protocols are left untouched. Ranges that
|
|
11
|
+
* cannot be resolved to a single exact version (e.g. `>=1.0.0`, `1.x`) are
|
|
12
|
+
* reported as unpinnable and must be fixed manually.
|
|
13
|
+
*/
|
|
14
|
+
export async function pinDependencies(options) {
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
const dependencyNames = [...new Set(options.dependencyNames)];
|
|
17
|
+
const { packages } = await readWorkspaceProject(cwd);
|
|
18
|
+
const pinned = [];
|
|
19
|
+
const unpinnable = [];
|
|
20
|
+
for (const { packageJson, packagePath } of packages) {
|
|
21
|
+
if (packageJson.private === true)
|
|
22
|
+
continue;
|
|
23
|
+
const name = packageJson.name ?? packagePath;
|
|
24
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
25
|
+
const dependencies = packageJson[field];
|
|
26
|
+
if (dependencies == null)
|
|
27
|
+
continue;
|
|
28
|
+
for (const dependencyName of dependencyNames) {
|
|
29
|
+
const range = dependencies[dependencyName];
|
|
30
|
+
if (typeof range !== 'string' || range.startsWith('workspace:')) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!/^[~^]/.test(range)) {
|
|
34
|
+
if (validVersion(range) == null) {
|
|
35
|
+
unpinnable.push({ packagePath, name, dependencyName, range });
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const exact = range.slice(1);
|
|
40
|
+
if (validVersion(exact) == null) {
|
|
41
|
+
unpinnable.push({ packagePath, name, dependencyName, range });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
dependencies[dependencyName] = exact;
|
|
45
|
+
pinned.push({ packagePath, name, dependencyName, range, exact });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const changedPackagePaths = new Set(pinned.map((pin) => pin.packagePath));
|
|
50
|
+
await Promise.all(packages
|
|
51
|
+
.filter(({ packagePath }) => changedPackagePaths.has(packagePath))
|
|
52
|
+
.map(({ packageJson, packagePath }) => writePackageJsonFile(path.join(cwd, packagePath), packageJson)));
|
|
53
|
+
return { pinned, unpinnable };
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sboudrias/package",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"cli",
|
|
6
6
|
"lint",
|
|
@@ -52,12 +52,13 @@
|
|
|
52
52
|
"@types/semver": "^7.7.1",
|
|
53
53
|
"type-fest": "^5.5.0",
|
|
54
54
|
"typescript": "^6.0.2",
|
|
55
|
-
"
|
|
55
|
+
"vite": "^8.3.0",
|
|
56
|
+
"vitest": "^5.0.0"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
58
59
|
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
|
|
59
60
|
},
|
|
60
61
|
"main": "./dist/index.js",
|
|
61
62
|
"types": "./dist/index.d.ts",
|
|
62
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "a9436bd94d55a439df308f50e77a7cd0b2078e98"
|
|
63
64
|
}
|
|
File without changes
|