@bonvoy/plugin-npm 0.1.1 → 0.2.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/README.md +83 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +50 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -5
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @bonvoy/plugin-npm 🚢
|
|
2
|
+
|
|
3
|
+
> npm publishing plugin for bonvoy
|
|
4
|
+
|
|
5
|
+
Publishes packages to the npm registry with OIDC provenance support.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @bonvoy/plugin-npm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- ✅ Publishes packages to npm registry
|
|
16
|
+
- ✅ OIDC provenance support for supply chain security
|
|
17
|
+
- ✅ Skips already published versions
|
|
18
|
+
- ✅ Skips private packages
|
|
19
|
+
- ✅ Configurable access level (public/restricted)
|
|
20
|
+
- ✅ Custom registry support
|
|
21
|
+
- ✅ Dry-run support
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// bonvoy.config.js
|
|
27
|
+
export default {
|
|
28
|
+
npm: {
|
|
29
|
+
registry: 'https://registry.npmjs.org', // default
|
|
30
|
+
access: 'public', // default for scoped packages
|
|
31
|
+
provenance: true, // default in CI
|
|
32
|
+
skipExisting: true, // default
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Hooks
|
|
38
|
+
|
|
39
|
+
This plugin taps into the following hooks:
|
|
40
|
+
|
|
41
|
+
| Hook | Action |
|
|
42
|
+
|------|--------|
|
|
43
|
+
| `publish` | Publishes packages to npm registry |
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
For OIDC provenance in GitHub Actions:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write
|
|
52
|
+
contents: read
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Behavior
|
|
56
|
+
|
|
57
|
+
During the `publish` hook:
|
|
58
|
+
|
|
59
|
+
1. Checks if package is private (skips if true)
|
|
60
|
+
2. Checks if version already exists on npm (skips if true)
|
|
61
|
+
3. Publishes with `npm publish --access public --provenance`
|
|
62
|
+
|
|
63
|
+
## Private Packages
|
|
64
|
+
|
|
65
|
+
Packages with `"private": true` in package.json are automatically skipped.
|
|
66
|
+
|
|
67
|
+
## Scoped Packages
|
|
68
|
+
|
|
69
|
+
Scoped packages (e.g., `@bonvoy/core`) default to `restricted` access on npm. Set `access: 'public'` to publish publicly.
|
|
70
|
+
|
|
71
|
+
## Custom Registry
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
export default {
|
|
75
|
+
npm: {
|
|
76
|
+
registry: 'https://npm.pkg.github.com',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -4,6 +4,8 @@ import { BonvoyPlugin } from "@bonvoy/core";
|
|
|
4
4
|
interface NpmOperations {
|
|
5
5
|
publish(args: string[], cwd: string): Promise<void>;
|
|
6
6
|
view(pkg: string, version: string): Promise<string | null>;
|
|
7
|
+
packageExists(pkg: string): Promise<boolean>;
|
|
8
|
+
hasToken(): Promise<boolean>;
|
|
7
9
|
}
|
|
8
10
|
declare const defaultNpmOperations: NpmOperations;
|
|
9
11
|
//#endregion
|
|
@@ -22,12 +24,14 @@ declare class NpmPlugin implements BonvoyPlugin {
|
|
|
22
24
|
constructor(config?: NpmPluginConfig, ops?: NpmOperations);
|
|
23
25
|
apply(bonvoy: {
|
|
24
26
|
hooks: {
|
|
27
|
+
validateRepo: any;
|
|
25
28
|
publish: any;
|
|
26
29
|
};
|
|
27
30
|
}): void;
|
|
28
31
|
private publishPackages;
|
|
29
32
|
private publishPackage;
|
|
30
33
|
private isAlreadyPublished;
|
|
34
|
+
private validatePackages;
|
|
31
35
|
}
|
|
32
36
|
//#endregion
|
|
33
37
|
export { type NpmOperations, type NpmPluginConfig, NpmPlugin as default, defaultNpmOperations };
|
package/dist/index.mjs
CHANGED
|
@@ -18,6 +18,21 @@ const defaultNpmOperations = {
|
|
|
18
18
|
} catch {
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
+
},
|
|
22
|
+
async packageExists(pkg) {
|
|
23
|
+
try {
|
|
24
|
+
await execa("npm", [
|
|
25
|
+
"view",
|
|
26
|
+
pkg,
|
|
27
|
+
"name"
|
|
28
|
+
], { stdio: "pipe" });
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async hasToken() {
|
|
35
|
+
return !!process.env.NPM_TOKEN || !!process.env.NODE_AUTH_TOKEN;
|
|
21
36
|
}
|
|
22
37
|
};
|
|
23
38
|
|
|
@@ -38,36 +53,65 @@ var NpmPlugin = class {
|
|
|
38
53
|
this.ops = ops ?? defaultNpmOperations;
|
|
39
54
|
}
|
|
40
55
|
apply(bonvoy) {
|
|
56
|
+
bonvoy.hooks.validateRepo.tapPromise(this.name, async (context) => {
|
|
57
|
+
await this.validatePackages(context);
|
|
58
|
+
});
|
|
41
59
|
bonvoy.hooks.publish.tapPromise(this.name, async (context) => {
|
|
42
60
|
if (context.isDryRun) {
|
|
43
|
-
|
|
61
|
+
context.logger.info("🔍 [dry-run] Would publish packages to npm");
|
|
44
62
|
return;
|
|
45
63
|
}
|
|
46
64
|
await this.publishPackages(context);
|
|
47
65
|
});
|
|
48
66
|
}
|
|
49
67
|
async publishPackages(context) {
|
|
50
|
-
const { packages } = context;
|
|
68
|
+
const { packages, logger } = context;
|
|
51
69
|
for (const pkg of packages) {
|
|
52
70
|
if (this.config.skipExisting && await this.isAlreadyPublished(pkg)) {
|
|
53
|
-
|
|
71
|
+
logger.info(`Skipping ${pkg.name}@${pkg.version} - already published`);
|
|
54
72
|
continue;
|
|
55
73
|
}
|
|
56
|
-
await this.publishPackage(pkg);
|
|
74
|
+
await this.publishPackage(pkg, logger);
|
|
57
75
|
}
|
|
58
76
|
}
|
|
59
|
-
async publishPackage(pkg) {
|
|
77
|
+
async publishPackage(pkg, logger) {
|
|
60
78
|
const args = [];
|
|
61
79
|
if (this.config.dryRun) args.push("--dry-run");
|
|
62
80
|
args.push("--access", this.config.access);
|
|
63
81
|
if (this.config.provenance) args.push("--provenance");
|
|
64
82
|
if (this.config.registry !== "https://registry.npmjs.org") args.push("--registry", this.config.registry);
|
|
65
|
-
|
|
83
|
+
logger.info(`Publishing ${pkg.name}@${pkg.version}...`);
|
|
66
84
|
await this.ops.publish(args, pkg.path);
|
|
67
85
|
}
|
|
68
86
|
async isAlreadyPublished(pkg) {
|
|
69
87
|
return await this.ops.view(pkg.name, pkg.version) === pkg.version;
|
|
70
88
|
}
|
|
89
|
+
async validatePackages(context) {
|
|
90
|
+
const { changedPackages, versions, logger } = context;
|
|
91
|
+
if (!versions) return;
|
|
92
|
+
const alreadyPublished = [];
|
|
93
|
+
const needsToken = [];
|
|
94
|
+
const hasToken = await this.ops.hasToken();
|
|
95
|
+
for (const pkg of changedPackages) {
|
|
96
|
+
const version = versions[pkg.name];
|
|
97
|
+
if (!version) continue;
|
|
98
|
+
if (await this.ops.view(pkg.name, version) === version) {
|
|
99
|
+
alreadyPublished.push(`${pkg.name}@${version}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!hasToken && this.config.provenance) {
|
|
103
|
+
if (!await this.ops.packageExists(pkg.name)) needsToken.push(pkg.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (alreadyPublished.length > 0) {
|
|
107
|
+
logger.error(`❌ npm versions already published: ${alreadyPublished.join(", ")}`);
|
|
108
|
+
throw new Error(`Cannot release: npm versions already exist (${alreadyPublished.join(", ")}). Bump to a new version.`);
|
|
109
|
+
}
|
|
110
|
+
if (needsToken.length > 0) {
|
|
111
|
+
logger.error(`❌ First publish requires NPM_TOKEN: ${needsToken.join(", ")}`);
|
|
112
|
+
throw new Error(`Cannot release with OIDC: packages don't exist on npm yet (${needsToken.join(", ")}). First publish requires NPM_TOKEN. Run: node scripts/publish-dummy-packages.mjs`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
71
115
|
};
|
|
72
116
|
|
|
73
117
|
//#endregion
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/operations.ts","../src/npm.ts"],"sourcesContent":["import { execa } from 'execa';\n\nexport interface NpmOperations {\n publish(args: string[], cwd: string): Promise<void>;\n view(pkg: string, version: string): Promise<string | null>;\n}\n\nexport const defaultNpmOperations: NpmOperations = {\n async publish(args, cwd) {\n await execa('npm', ['publish', ...args], { cwd, stdio: 'inherit' });\n },\n\n async view(pkg, version) {\n try {\n const result = await execa('npm', ['view', `${pkg}@${version}`, 'version'], {\n stdio: 'pipe',\n });\n return result.stdout.trim() || null;\n } catch {\n return null;\n }\n },\n};\n","import type { BonvoyPlugin, PublishContext } from '@bonvoy/core';\n\nimport { defaultNpmOperations, type NpmOperations } from './operations.js';\n\nexport interface NpmPluginConfig {\n registry?: string;\n access?: 'public' | 'restricted';\n dryRun?: boolean;\n skipExisting?: boolean;\n provenance?: boolean;\n}\n\nexport default class NpmPlugin implements BonvoyPlugin {\n name = 'npm';\n\n private config: Required<NpmPluginConfig>;\n private ops: NpmOperations;\n\n constructor(config: NpmPluginConfig = {}, ops?: NpmOperations) {\n this.config = {\n registry: config.registry ?? 'https://registry.npmjs.org',\n access: config.access ?? 'public',\n dryRun: config.dryRun ?? false,\n skipExisting: config.skipExisting ?? true,\n provenance: config.provenance ?? true,\n };\n this.ops = ops ?? defaultNpmOperations;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Hook types are complex and vary by implementation\n apply(bonvoy: { hooks: { publish: any } }): void {\n bonvoy.hooks.publish.tapPromise(this.name, async (context: PublishContext) => {\n if (context.isDryRun) {\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/operations.ts","../src/npm.ts"],"sourcesContent":["import { execa } from 'execa';\n\nexport interface NpmOperations {\n publish(args: string[], cwd: string): Promise<void>;\n view(pkg: string, version: string): Promise<string | null>;\n packageExists(pkg: string): Promise<boolean>;\n hasToken(): Promise<boolean>;\n}\n\nexport const defaultNpmOperations: NpmOperations = {\n async publish(args, cwd) {\n await execa('npm', ['publish', ...args], { cwd, stdio: 'inherit' });\n },\n\n async view(pkg, version) {\n try {\n const result = await execa('npm', ['view', `${pkg}@${version}`, 'version'], {\n stdio: 'pipe',\n });\n return result.stdout.trim() || null;\n } catch {\n return null;\n }\n },\n\n /* c8 ignore start - real npm operations */\n async packageExists(pkg) {\n try {\n await execa('npm', ['view', pkg, 'name'], { stdio: 'pipe' });\n return true;\n } catch {\n return false;\n }\n },\n\n async hasToken() {\n return !!process.env.NPM_TOKEN || !!process.env.NODE_AUTH_TOKEN;\n },\n /* c8 ignore stop */\n};\n","import type { BonvoyPlugin, Context, PublishContext } from '@bonvoy/core';\n\nimport { defaultNpmOperations, type NpmOperations } from './operations.js';\n\nexport interface NpmPluginConfig {\n registry?: string;\n access?: 'public' | 'restricted';\n dryRun?: boolean;\n skipExisting?: boolean;\n provenance?: boolean;\n}\n\nexport default class NpmPlugin implements BonvoyPlugin {\n name = 'npm';\n\n private config: Required<NpmPluginConfig>;\n private ops: NpmOperations;\n\n constructor(config: NpmPluginConfig = {}, ops?: NpmOperations) {\n this.config = {\n registry: config.registry ?? 'https://registry.npmjs.org',\n access: config.access ?? 'public',\n dryRun: config.dryRun ?? false,\n skipExisting: config.skipExisting ?? true,\n provenance: config.provenance ?? true,\n };\n this.ops = ops ?? defaultNpmOperations;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Hook types are complex and vary by implementation\n apply(bonvoy: { hooks: { validateRepo: any; publish: any } }): void {\n bonvoy.hooks.validateRepo.tapPromise(this.name, async (context: Context) => {\n await this.validatePackages(context);\n });\n\n bonvoy.hooks.publish.tapPromise(this.name, async (context: PublishContext) => {\n if (context.isDryRun) {\n context.logger.info('🔍 [dry-run] Would publish packages to npm');\n return;\n }\n await this.publishPackages(context);\n });\n }\n\n private async publishPackages(context: PublishContext): Promise<void> {\n const { packages, logger } = context;\n\n for (const pkg of packages) {\n if (this.config.skipExisting && (await this.isAlreadyPublished(pkg))) {\n logger.info(`Skipping ${pkg.name}@${pkg.version} - already published`);\n continue;\n }\n\n await this.publishPackage(pkg, logger);\n }\n }\n\n private async publishPackage(\n pkg: { name: string; version: string; path: string },\n logger: PublishContext['logger'],\n ): Promise<void> {\n const args: string[] = [];\n\n if (this.config.dryRun) {\n args.push('--dry-run');\n }\n\n args.push('--access', this.config.access);\n\n if (this.config.provenance) {\n args.push('--provenance');\n }\n\n if (this.config.registry !== 'https://registry.npmjs.org') {\n args.push('--registry', this.config.registry);\n }\n\n logger.info(`Publishing ${pkg.name}@${pkg.version}...`);\n\n await this.ops.publish(args, pkg.path);\n }\n\n private async isAlreadyPublished(pkg: { name: string; version: string }): Promise<boolean> {\n const version = await this.ops.view(pkg.name, pkg.version);\n return version === pkg.version;\n }\n\n private async validatePackages(context: Context): Promise<void> {\n const { changedPackages, versions, logger } = context;\n if (!versions) return;\n\n const alreadyPublished: string[] = [];\n const needsToken: string[] = [];\n const hasToken = await this.ops.hasToken();\n\n for (const pkg of changedPackages) {\n const version = versions[pkg.name];\n if (!version) continue;\n\n // Check if version already exists\n const existingVersion = await this.ops.view(pkg.name, version);\n if (existingVersion === version) {\n alreadyPublished.push(`${pkg.name}@${version}`);\n continue;\n }\n\n // Check if package exists (for OIDC)\n if (!hasToken && this.config.provenance) {\n const exists = await this.ops.packageExists(pkg.name);\n if (!exists) {\n needsToken.push(pkg.name);\n }\n }\n }\n\n if (alreadyPublished.length > 0) {\n logger.error(`❌ npm versions already published: ${alreadyPublished.join(', ')}`);\n throw new Error(\n `Cannot release: npm versions already exist (${alreadyPublished.join(', ')}). Bump to a new version.`,\n );\n }\n\n if (needsToken.length > 0) {\n logger.error(`❌ First publish requires NPM_TOKEN: ${needsToken.join(', ')}`);\n throw new Error(\n `Cannot release with OIDC: packages don't exist on npm yet (${needsToken.join(', ')}). First publish requires NPM_TOKEN. Run: node scripts/publish-dummy-packages.mjs`,\n );\n }\n }\n}\n\nexport { defaultNpmOperations, type NpmOperations } from './operations.js';\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonvoy/plugin-npm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "npm publishing plugin for bonvoy",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "🚢 npm publishing plugin for bonvoy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bonvoy",
|
|
7
7
|
"plugin",
|
|
@@ -38,9 +38,6 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"execa": "^9.6.1"
|
|
40
40
|
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"vitest": "^4.0.16"
|
|
43
|
-
},
|
|
44
41
|
"engines": {
|
|
45
42
|
"node": ">= 20.5"
|
|
46
43
|
}
|