@sboudrias/package 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,17 @@ bun add @sboudrias/package --dev
43
43
  </tr>
44
44
  </table>
45
45
 
46
- # Usage
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
- # Lint Rules
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
- ## Matching engines
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
- ## Ensure package.json is exposed
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 linted as a single-package project.
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, Command, Option } from 'clipanion';
3
- import { lintPackages } from "./lint.js";
4
- class LintCommand extends Command {
5
- static paths = [['lint']];
6
- static usage = Command.Usage({
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
@@ -1 +1,3 @@
1
- export { lintPackages } from './lint.ts';
1
+ export { lintPackages } from './lint/index.ts';
2
+ export { normalizeManifests } from './normalize/index.ts';
3
+ export { pinDependencies } from './pin/index.ts';
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
- export { lintPackages } from "./lint.js";
1
+ export { lintPackages } from "./lint/index.js";
2
+ export { normalizeManifests } from "./normalize/index.js";
3
+ export { pinDependencies } from "./pin/index.js";
@@ -0,0 +1,7 @@
1
+ import { Command } from 'clipanion';
2
+ export declare class LintCommand extends Command {
3
+ static paths: string[][];
4
+ static usage: import("clipanion").Usage;
5
+ check: boolean;
6
+ execute(): Promise<0 | 1>;
7
+ }
@@ -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 "./peer-dependencies.js";
4
- import { resolveDependencyPackageJson, writePackageJsonFile } from "./package-json.js";
5
- import { readWorkspaceProject } from "./workspaces.js";
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,6 @@
1
+ import { Command } from 'clipanion';
2
+ export declare class NormalizeCommand extends Command {
3
+ static paths: string[][];
4
+ static usage: import("clipanion").Usage;
5
+ execute(): Promise<0 | 1>;
6
+ }
@@ -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,7 @@
1
+ import { Command } from 'clipanion';
2
+ export declare class PinCommand extends Command {
3
+ static paths: string[][];
4
+ static usage: import("clipanion").Usage;
5
+ dependencyName: string;
6
+ execute(): Promise<0 | 1>;
7
+ }
@@ -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.2",
3
+ "version": "1.0.3",
4
4
  "keywords": [
5
5
  "cli",
6
6
  "lint",
@@ -59,5 +59,5 @@
59
59
  },
60
60
  "main": "./dist/index.js",
61
61
  "types": "./dist/index.d.ts",
62
- "gitHead": "2073546b459fd91d71f4f778ddf7b4c5417bb8b5"
62
+ "gitHead": "5748bd9966b5e175ddf5995ee4cdce6f60c4272e"
63
63
  }
File without changes