@sboudrias/package 1.0.0
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 +22 -0
- package/README.md +88 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lint.d.ts +11 -0
- package/dist/lint.js +120 -0
- package/dist/package-json.d.ts +4 -0
- package/dist/package-json.js +85 -0
- package/dist/peer-dependencies.d.ts +20 -0
- package/dist/peer-dependencies.js +66 -0
- package/dist/workspaces.d.ts +12 -0
- package/dist/workspaces.js +86 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Simon Boudrias
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated documentation
|
|
5
|
+
files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @sboudrias/package
|
|
2
|
+
|
|
3
|
+
Package metadata tools for JavaScript packages and monorepos.
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
|
|
7
|
+
<table>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>npm</th>
|
|
10
|
+
<th>yarn</th>
|
|
11
|
+
<th>pnpm</th>
|
|
12
|
+
<th>bun</th>
|
|
13
|
+
</tr>
|
|
14
|
+
<tr>
|
|
15
|
+
<td>
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @sboudrias/package --save-dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
</td>
|
|
22
|
+
<td>
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
yarn add @sboudrias/package --dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
</td>
|
|
29
|
+
<td>
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add @sboudrias/package --save-dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
</td>
|
|
36
|
+
<td>
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bun add @sboudrias/package --dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
</td>
|
|
43
|
+
</tr>
|
|
44
|
+
</table>
|
|
45
|
+
|
|
46
|
+
# Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
package lint
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`package lint` validates public workspace packages and fixes safe package metadata issues in place.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
package lint --check
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`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
|
+
|
|
60
|
+
# Lint Rules
|
|
61
|
+
|
|
62
|
+
## Valid Peer Dependencies
|
|
63
|
+
|
|
64
|
+
Runtime dependencies can declare their own peer dependencies. `package lint` makes those peer requirements visible on the package that uses the runtime dependency.
|
|
65
|
+
|
|
66
|
+
It adds missing peers to `peerDependencies` and copies matching `peerDependenciesMeta` entries so optional peers stay optional.
|
|
67
|
+
|
|
68
|
+
## Matching engines
|
|
69
|
+
|
|
70
|
+
Packages should only advertise Node.js support that their runtime dependencies can also support.
|
|
71
|
+
|
|
72
|
+
`package lint` sets missing, invalid, or out-of-root-range `engines.node` values to the root package `engines.node` range. It fails when a runtime dependency supports a narrower Node.js range than the package.
|
|
73
|
+
|
|
74
|
+
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
|
+
|
|
76
|
+
## Ensure package.json is exposed
|
|
77
|
+
|
|
78
|
+
Packages should expose their manifest for tools that inspect package metadata at runtime.
|
|
79
|
+
|
|
80
|
+
`package lint` ensures public packages expose `"./package.json": "./package.json"` in `exports`.
|
|
81
|
+
|
|
82
|
+
# Workspace Discovery
|
|
83
|
+
|
|
84
|
+
The CLI discovers workspaces from `package.json` `workspaces` fields and `pnpm-workspace.yaml` files.
|
|
85
|
+
|
|
86
|
+
If no workspaces are configured, the root `package.json` is linted as a single-package project.
|
|
87
|
+
|
|
88
|
+
Private packages are ignored by default.
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { Builtins, Cli, Command, Option } from 'clipanion';
|
|
5
|
+
import { lintPackages } from "./lint.js";
|
|
6
|
+
class LintCommand extends Command {
|
|
7
|
+
static paths = [['lint']];
|
|
8
|
+
static usage = Command.Usage({
|
|
9
|
+
description: 'Validate and fix package metadata.',
|
|
10
|
+
examples: [
|
|
11
|
+
['Fix package metadata', '$0 lint'],
|
|
12
|
+
['Validate package metadata without writing files', '$0 lint --check'],
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
check = Option.Boolean('--check', false, {
|
|
16
|
+
description: 'Validate package metadata without writing files.',
|
|
17
|
+
});
|
|
18
|
+
async execute() {
|
|
19
|
+
try {
|
|
20
|
+
const result = await lintPackages({ check: this.check });
|
|
21
|
+
for (const issue of result.issues) {
|
|
22
|
+
this.context.stderr.write(`[${issue.status}] ${issue.message} (${issue.packagePath})\n`);
|
|
23
|
+
}
|
|
24
|
+
return result.hasFailures ? 1 : 0;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
this.context.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const cli = Cli.from([LintCommand, Builtins.HelpCommand], {
|
|
33
|
+
binaryLabel: 'Package',
|
|
34
|
+
binaryName: 'package',
|
|
35
|
+
enableCapture: false,
|
|
36
|
+
});
|
|
37
|
+
export async function main(argv = process.argv.slice(2), context = {}) {
|
|
38
|
+
return cli.run(argv, {
|
|
39
|
+
...Cli.defaultContext,
|
|
40
|
+
...context,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (process.argv[1] != null &&
|
|
44
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
45
|
+
process.exitCode = await main();
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { lintPackages } from './lint.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { lintPackages } from "./lint.js";
|
package/dist/lint.d.ts
ADDED
package/dist/lint.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
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";
|
|
6
|
+
function nodeEngine(pkg) {
|
|
7
|
+
const node = pkg.engines?.['node'];
|
|
8
|
+
return typeof node === 'string' ? node : undefined;
|
|
9
|
+
}
|
|
10
|
+
export async function lintPackages(options = {}) {
|
|
11
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12
|
+
const check = options.check ?? false;
|
|
13
|
+
const { rootPackageJson, packages } = await readWorkspaceProject(cwd);
|
|
14
|
+
const rootNodeRange = nodeEngine(rootPackageJson);
|
|
15
|
+
if (rootNodeRange == null || validRange(rootNodeRange) == null) {
|
|
16
|
+
throw new Error('The root package.json must define a valid engines.node range.');
|
|
17
|
+
}
|
|
18
|
+
const publicPackages = packages.filter(({ packageJson }) => packageJson.private !== true);
|
|
19
|
+
const packagesByName = new Map(packages.flatMap(({ packageJson }) => packageJson.name == null ? [] : [[packageJson.name, packageJson]]));
|
|
20
|
+
const resolvePackageJson = (dependencyName, packageDir) => packagesByName.get(dependencyName) ??
|
|
21
|
+
resolveDependencyPackageJson(dependencyName, packageDir);
|
|
22
|
+
const originalPackages = new Map(publicPackages.map(({ packageJson, packagePath }) => [
|
|
23
|
+
packagePath,
|
|
24
|
+
JSON.stringify(packageJson),
|
|
25
|
+
]));
|
|
26
|
+
const issues = [];
|
|
27
|
+
const report = (packagePath, message, fixable) => {
|
|
28
|
+
issues.push({
|
|
29
|
+
packagePath,
|
|
30
|
+
message,
|
|
31
|
+
status: fixable ? (check ? 'would fix' : 'fixed') : 'error',
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
for (const { packageDir, packageJson, packagePath } of publicPackages) {
|
|
35
|
+
const name = packageJson.name ?? packagePath;
|
|
36
|
+
const { hoists } = getPeerDependencyHoists(packageJson, packageDir, resolvePackageJson);
|
|
37
|
+
for (const hoist of hoists) {
|
|
38
|
+
report(packagePath, hoist.kind === 'peer-dependency'
|
|
39
|
+
? `${name} must hoist ${hoist.peerName} from ${hoist.dependencyName}.`
|
|
40
|
+
: `${name} must copy peer dependency metadata for ${hoist.peerName} from ${hoist.dependencyName}.`, true);
|
|
41
|
+
}
|
|
42
|
+
if (!check)
|
|
43
|
+
applyPeerDependencyHoists(packageJson, hoists);
|
|
44
|
+
let packageRange = nodeEngine(packageJson);
|
|
45
|
+
if (packageRange == null ||
|
|
46
|
+
validRange(packageRange) == null ||
|
|
47
|
+
!subset(packageRange, rootNodeRange)) {
|
|
48
|
+
report(packagePath, `${name} must define an engines.node range within the root range "${rootNodeRange}".`, true);
|
|
49
|
+
if (check) {
|
|
50
|
+
packageRange = undefined;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
packageJson.engines ??= {};
|
|
54
|
+
packageJson.engines['node'] = rootNodeRange;
|
|
55
|
+
packageRange = rootNodeRange;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (packageRange != null) {
|
|
59
|
+
for (const dependencyName of new Set([
|
|
60
|
+
...Object.keys(packageJson.dependencies ?? {}),
|
|
61
|
+
...Object.keys(packageJson.optionalDependencies ?? {}),
|
|
62
|
+
])) {
|
|
63
|
+
const dependencyPackage = resolvePackageJson(dependencyName, packageDir);
|
|
64
|
+
if (dependencyPackage == null) {
|
|
65
|
+
report(packagePath, `${name} depends on ${dependencyName}, but its package.json could not be resolved.`, false);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const dependencyRange = nodeEngine(dependencyPackage);
|
|
69
|
+
if (dependencyRange == null)
|
|
70
|
+
continue;
|
|
71
|
+
if (validRange(dependencyRange) == null) {
|
|
72
|
+
report(packagePath, `${name} depends on ${dependencyName}, which has an invalid engines.node range: ${dependencyRange}`, false);
|
|
73
|
+
}
|
|
74
|
+
else if (!subset(packageRange, dependencyRange)) {
|
|
75
|
+
report(packagePath, `${name} supports node "${packageRange}", but ${dependencyName} supports "${dependencyRange}".`, false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const currentExports = packageJson.exports;
|
|
80
|
+
const currentExportsObject = currentExports != null &&
|
|
81
|
+
typeof currentExports === 'object' &&
|
|
82
|
+
!Array.isArray(currentExports)
|
|
83
|
+
? currentExports
|
|
84
|
+
: undefined;
|
|
85
|
+
if (currentExportsObject != null &&
|
|
86
|
+
Reflect.get(currentExportsObject, './package.json') === './package.json') {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (currentExports == null ||
|
|
90
|
+
typeof currentExports === 'string' ||
|
|
91
|
+
currentExportsObject != null) {
|
|
92
|
+
report(packagePath, `${name} must export ./package.json.`, true);
|
|
93
|
+
if (!check) {
|
|
94
|
+
if (currentExports == null) {
|
|
95
|
+
packageJson.exports = { './package.json': './package.json' };
|
|
96
|
+
}
|
|
97
|
+
else if (typeof currentExports === 'string') {
|
|
98
|
+
packageJson.exports = {
|
|
99
|
+
'.': currentExports,
|
|
100
|
+
'./package.json': './package.json',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else if (currentExportsObject != null) {
|
|
104
|
+
Reflect.set(currentExportsObject, './package.json', './package.json');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
report(packagePath, `${name} uses an unsupported exports shape; add ./package.json manually.`, false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const changedPackages = publicPackages.filter(({ packageJson, packagePath }) => JSON.stringify(packageJson) !== originalPackages.get(packagePath));
|
|
113
|
+
if (!check) {
|
|
114
|
+
await Promise.all(changedPackages.map(({ packageJson, packagePath }) => writePackageJsonFile(path.join(cwd, packagePath), packageJson)));
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
issues,
|
|
118
|
+
hasFailures: issues.some((issue) => issue.status !== 'fixed'),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PackageJson } from 'type-fest';
|
|
2
|
+
export declare function readPackageJsonFile(filepath: string): PackageJson;
|
|
3
|
+
export declare function writePackageJsonFile(filepath: string, packageJson: PackageJson): Promise<void>;
|
|
4
|
+
export declare function resolveDependencyPackageJson(name: string, fromDirectory?: string): PackageJson | undefined;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsPromises from 'node:fs/promises';
|
|
3
|
+
import Module from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
6
|
+
function asPackageJson(value, source) {
|
|
7
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
8
|
+
throw new TypeError(`Expected ${source} to contain a package.json object`);
|
|
9
|
+
}
|
|
10
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function errorCode(error) {
|
|
14
|
+
return error && typeof error === 'object' && 'code' in error
|
|
15
|
+
? String(error.code)
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
function createRequireFrom(fromDirectory) {
|
|
19
|
+
return Module.createRequire(path.join(fromDirectory, 'package.json'));
|
|
20
|
+
}
|
|
21
|
+
function findPackageJson(fromDirectory) {
|
|
22
|
+
let directory = fromDirectory;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const packageJsonPath = path.join(directory, 'package.json');
|
|
25
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
26
|
+
return packageJsonPath;
|
|
27
|
+
}
|
|
28
|
+
const parent = path.dirname(directory);
|
|
29
|
+
if (parent === directory) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
directory = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function findNodeModulesPackageJson(name, fromDirectory) {
|
|
36
|
+
let directory = fromDirectory;
|
|
37
|
+
for (;;) {
|
|
38
|
+
const packageJsonPath = path.join(directory, 'node_modules', name, 'package.json');
|
|
39
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
40
|
+
return packageJsonPath;
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(directory);
|
|
43
|
+
if (parent === directory) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
directory = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function readPackageJsonFile(filepath) {
|
|
50
|
+
return asPackageJson(parseJsonc(fs.readFileSync(filepath, 'utf8')), filepath);
|
|
51
|
+
}
|
|
52
|
+
export async function writePackageJsonFile(filepath, packageJson) {
|
|
53
|
+
await fsPromises.writeFile(filepath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
54
|
+
}
|
|
55
|
+
export function resolveDependencyPackageJson(name, fromDirectory = process.cwd()) {
|
|
56
|
+
const requireFromDirectory = createRequireFrom(fromDirectory);
|
|
57
|
+
try {
|
|
58
|
+
const packageJson = requireFromDirectory(`${name}/package.json`);
|
|
59
|
+
return asPackageJson(packageJson, `${name}/package.json`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = errorCode(error);
|
|
63
|
+
if (code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && code !== 'MODULE_NOT_FOUND') {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const entrypoint = requireFromDirectory.resolve(name);
|
|
69
|
+
const entrypointPackageJsonPath = findPackageJson(path.dirname(entrypoint));
|
|
70
|
+
if (entrypointPackageJsonPath != null) {
|
|
71
|
+
return readPackageJsonFile(entrypointPackageJsonPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const code = errorCode(error);
|
|
76
|
+
if (code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && code !== 'MODULE_NOT_FOUND') {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const packageJsonPath = findNodeModulesPackageJson(name, fromDirectory);
|
|
81
|
+
if (packageJsonPath != null) {
|
|
82
|
+
return readPackageJsonFile(packageJsonPath);
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PackageJson } from 'type-fest';
|
|
2
|
+
export type ResolvePackageJson = (dependencyName: string, fromDirectory: string) => PackageJson | undefined;
|
|
3
|
+
export type PeerDependencyHoist = {
|
|
4
|
+
kind: 'peer-dependency';
|
|
5
|
+
dependencyName: string;
|
|
6
|
+
peerName: string;
|
|
7
|
+
peerVersion: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'peer-dependency-meta';
|
|
10
|
+
dependencyName: string;
|
|
11
|
+
peerName: string;
|
|
12
|
+
peerMeta: NonNullable<PackageJson['peerDependenciesMeta']>[string];
|
|
13
|
+
};
|
|
14
|
+
export type PeerDependencyHoistResult = {
|
|
15
|
+
hoists: PeerDependencyHoist[];
|
|
16
|
+
missingDependencies: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function getPeerDependencyHoists(pkg: PackageJson, packageDir: string, resolvePackageJson?: ResolvePackageJson): PeerDependencyHoistResult;
|
|
19
|
+
export declare function applyPeerDependencyHoists(pkg: PackageJson, hoists: PeerDependencyHoist[]): void;
|
|
20
|
+
export declare function hoistPeerDependencies(pkg: PackageJson, packageDir: string, resolvePackageJson?: ResolvePackageJson): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolveDependencyPackageJson } from "./package-json.js";
|
|
2
|
+
function runtimeDependencyNames(pkg) {
|
|
3
|
+
return new Set([
|
|
4
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
5
|
+
...Object.keys(pkg.optionalDependencies ?? {}),
|
|
6
|
+
]);
|
|
7
|
+
}
|
|
8
|
+
function hasRuntimeDependency(pkg, dependencyName) {
|
|
9
|
+
return (pkg.dependencies?.[dependencyName] != null ||
|
|
10
|
+
pkg.optionalDependencies?.[dependencyName] != null);
|
|
11
|
+
}
|
|
12
|
+
export function getPeerDependencyHoists(pkg, packageDir, resolvePackageJson = resolveDependencyPackageJson) {
|
|
13
|
+
const hoists = [];
|
|
14
|
+
const missingDependencies = [];
|
|
15
|
+
for (const dependencyName of runtimeDependencyNames(pkg)) {
|
|
16
|
+
const dependencyPackage = resolvePackageJson(dependencyName, packageDir);
|
|
17
|
+
if (dependencyPackage == null) {
|
|
18
|
+
missingDependencies.push(dependencyName);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const peerName of Object.keys(dependencyPackage.peerDependencies ?? {})) {
|
|
22
|
+
const peerVersion = dependencyPackage.peerDependencies?.[peerName];
|
|
23
|
+
if (typeof peerVersion !== 'string')
|
|
24
|
+
continue;
|
|
25
|
+
let packageShouldHavePeer = !hasRuntimeDependency(pkg, peerName) && pkg.peerDependencies?.[peerName] == null;
|
|
26
|
+
if (packageShouldHavePeer) {
|
|
27
|
+
hoists.push({
|
|
28
|
+
kind: 'peer-dependency',
|
|
29
|
+
dependencyName,
|
|
30
|
+
peerName,
|
|
31
|
+
peerVersion,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
packageShouldHavePeer ||= pkg.peerDependencies?.[peerName] != null;
|
|
35
|
+
const peerMeta = dependencyPackage.peerDependenciesMeta?.[peerName];
|
|
36
|
+
if (packageShouldHavePeer &&
|
|
37
|
+
peerMeta != null &&
|
|
38
|
+
pkg.peerDependenciesMeta?.[peerName] == null) {
|
|
39
|
+
hoists.push({
|
|
40
|
+
kind: 'peer-dependency-meta',
|
|
41
|
+
dependencyName,
|
|
42
|
+
peerName,
|
|
43
|
+
peerMeta,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { hoists, missingDependencies };
|
|
49
|
+
}
|
|
50
|
+
export function applyPeerDependencyHoists(pkg, hoists) {
|
|
51
|
+
for (const hoist of hoists) {
|
|
52
|
+
if (hoist.kind === 'peer-dependency') {
|
|
53
|
+
pkg.peerDependencies ??= {};
|
|
54
|
+
pkg.peerDependencies[hoist.peerName] = hoist.peerVersion;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
pkg.peerDependenciesMeta ??= {};
|
|
58
|
+
pkg.peerDependenciesMeta[hoist.peerName] = hoist.peerMeta;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function hoistPeerDependencies(pkg, packageDir, resolvePackageJson) {
|
|
63
|
+
const { hoists } = getPeerDependencyHoists(pkg, packageDir, resolvePackageJson);
|
|
64
|
+
applyPeerDependencyHoists(pkg, hoists);
|
|
65
|
+
return hoists.length > 0;
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PackageJson } from 'type-fest';
|
|
2
|
+
export type WorkspacePackage = {
|
|
3
|
+
packageJson: PackageJson;
|
|
4
|
+
packagePath: string;
|
|
5
|
+
packageDir: string;
|
|
6
|
+
};
|
|
7
|
+
export type WorkspaceProject = {
|
|
8
|
+
rootPackageJson: PackageJson;
|
|
9
|
+
rootPackagePath: string;
|
|
10
|
+
packages: WorkspacePackage[];
|
|
11
|
+
};
|
|
12
|
+
export declare function readWorkspaceProject(cwd?: string): Promise<WorkspaceProject>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { globby } from 'globby';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { readPackageJsonFile } from "./package-json.js";
|
|
6
|
+
function errorCode(error) {
|
|
7
|
+
return error && typeof error === 'object' && 'code' in error
|
|
8
|
+
? String(error.code)
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
function workspacePackagePattern(pattern) {
|
|
12
|
+
const isNegated = pattern.startsWith('!');
|
|
13
|
+
const workspacePattern = isNegated ? pattern.slice(1) : pattern;
|
|
14
|
+
const packageJsonPattern = workspacePattern.endsWith('/package.json')
|
|
15
|
+
? workspacePattern
|
|
16
|
+
: `${workspacePattern.replace(/\/$/, '')}/package.json`;
|
|
17
|
+
return isNegated ? `!${packageJsonPattern}` : packageJsonPattern;
|
|
18
|
+
}
|
|
19
|
+
function packageJsonWorkspacePatterns(rootPackageJson) {
|
|
20
|
+
const workspaces = rootPackageJson.workspaces;
|
|
21
|
+
if (Array.isArray(workspaces)) {
|
|
22
|
+
return workspaces.filter((workspace) => typeof workspace === 'string');
|
|
23
|
+
}
|
|
24
|
+
if (workspaces != null && typeof workspaces === 'object') {
|
|
25
|
+
const packages = 'packages' in workspaces ? workspaces.packages : undefined;
|
|
26
|
+
if (Array.isArray(packages)) {
|
|
27
|
+
return packages.filter((workspace) => typeof workspace === 'string');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
function pnpmWorkspacePatternsFrom(value) {
|
|
33
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const packages = 'packages' in value ? value.packages : undefined;
|
|
37
|
+
if (!Array.isArray(packages)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return packages.filter((workspace) => typeof workspace === 'string');
|
|
41
|
+
}
|
|
42
|
+
async function readPnpmWorkspacePatterns(cwd) {
|
|
43
|
+
for (const filename of ['pnpm-workspace.yaml', 'pnpm-workspace.yml']) {
|
|
44
|
+
try {
|
|
45
|
+
const content = await fs.readFile(path.join(cwd, filename), 'utf8');
|
|
46
|
+
return pnpmWorkspacePatternsFrom(parseYaml(content));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (errorCode(error) !== 'ENOENT') {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
export async function readWorkspaceProject(cwd = process.cwd()) {
|
|
57
|
+
const rootPackagePath = path.join(cwd, 'package.json');
|
|
58
|
+
const rootPackageJson = readPackageJsonFile(rootPackagePath);
|
|
59
|
+
const workspacePatterns = [
|
|
60
|
+
...packageJsonWorkspacePatterns(rootPackageJson),
|
|
61
|
+
...(await readPnpmWorkspacePatterns(cwd)),
|
|
62
|
+
];
|
|
63
|
+
if (workspacePatterns.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
rootPackageJson,
|
|
66
|
+
rootPackagePath,
|
|
67
|
+
packages: [
|
|
68
|
+
{
|
|
69
|
+
packageJson: rootPackageJson,
|
|
70
|
+
packagePath: 'package.json',
|
|
71
|
+
packageDir: cwd,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const packagePaths = await globby([...new Set(workspacePatterns.map(workspacePackagePattern)), '!**/node_modules'], { cwd });
|
|
77
|
+
const packages = packagePaths.toSorted().map((packagePath) => {
|
|
78
|
+
const absolutePackagePath = path.join(cwd, packagePath);
|
|
79
|
+
return {
|
|
80
|
+
packageJson: readPackageJsonFile(absolutePackagePath),
|
|
81
|
+
packagePath,
|
|
82
|
+
packageDir: path.dirname(absolutePackagePath),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return { rootPackageJson, rootPackagePath, packages };
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sboudrias/package",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"cli",
|
|
6
|
+
"lint",
|
|
7
|
+
"monorepo",
|
|
8
|
+
"node",
|
|
9
|
+
"npm",
|
|
10
|
+
"package-json",
|
|
11
|
+
"pnpm",
|
|
12
|
+
"workspace",
|
|
13
|
+
"yarn"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/main/tools/package/README.md",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Simon Boudrias <admin@simonboudrias.com>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/SBoudrias/Inquirer.js.git"
|
|
21
|
+
},
|
|
22
|
+
"bin": "./dist/bin.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"tsc": "tsc && chmod +x ./dist/bin.js"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"clipanion": "^4.0.0-rc.4",
|
|
43
|
+
"globby": "^16.2.0",
|
|
44
|
+
"jsonc-parser": "^3.3.1",
|
|
45
|
+
"semver": "^7.7.4",
|
|
46
|
+
"typanion": "^3.14.0",
|
|
47
|
+
"yaml": "^2.9.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@repo/tsconfig": "workspace:*",
|
|
51
|
+
"@types/node": "^25.5.2",
|
|
52
|
+
"@types/semver": "^7.7.1",
|
|
53
|
+
"type-fest": "^5.5.0",
|
|
54
|
+
"typescript": "^6.0.2",
|
|
55
|
+
"vitest": "^4.1.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
|
|
59
|
+
},
|
|
60
|
+
"main": "./dist/index.js",
|
|
61
|
+
"types": "./dist/index.d.ts",
|
|
62
|
+
"gitHead": "bfd8710229e3d3c9784bef3bbfbd84c7bd09bb9e"
|
|
63
|
+
}
|