Skip to content

Commit

Permalink
add docs for bun publish (#14327)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylan-conway authored Oct 3, 2024
1 parent eda608d commit 39b1c01
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 91 deletions.
111 changes: 111 additions & 0 deletions docs/cli/publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## `bun publish`

Use `bun publish` to publish a package to the npm registry.

`bun publish` will automatically pack your package into a tarball, strip workspace protocols from the `package.json` (resolving versions if necessary), and publish to the registry specified in your configuration files. Both `bunfig.toml` and `.npmrc` files are supported.

```sh
## Publishing the package from the current working directory
$ bun publish

## Output
bun publish v1.1.30 (ca7428e9)

packed 203B package.json
packed 224B README.md
packed 30B index.ts
packed 0.64KB tsconfig.json

Total files: 4
Shasum: 79e2b4377b63f4de38dc7ea6e5e9dbee08311a69
Integrity: sha512-6QSNlDdSwyG/+[...]X6wXHriDWr6fA==
Unpacked size: 1.1KB
Packed size: 0.76KB
Tag: latest
Access: default
Registry: http://localhost:4873/

+ [email protected]
```

Alternatively, you can pack and publish your package separately by using `bun pm pack` followed by `bun publish` with the path to the output tarball.

```sh
$ bun pm pack
...
$ bun publish ./package.tgz
```

{% callout %}
**Note** - `bun publish` will not run lifecycle scripts (`prepublishOnly/prepack/prepare/postpack/publish/postpublish`) if a tarball path is provided. Scripts will only be run if the package is packed by `bun publish`.
{% /callout %}

## Flags

### `--access`

The `--access` flag can be used to set the access level of the package being published. The access level can be one of `public` or `restricted`. Uscoped packages are always public, and attempting to publish an unscoped package with `--access restricted` will result in an error.

```sh
$ bun publish --access public
```

`--access` can also be set in the `publishConfig` field of your `package.json`.

```json
{
"publishConfig": {
"access": "restricted"
}
}
```

### `--tag`

Set the tag of the package version being published. By default, the tag is `latest`. The initial version of a package is always given the `latest` tag in addition to the specified tag.

```sh
$ bun publish --tag alpha
```

`--tag` can also be set in the `publishConfig` field of your `package.json`.

```json
{
"publishConfig": {
"tag": "next"
}
}
```

### `--dry-run`

The `--dry-run` flag can be used to simulate the publish process without actually publishing the package. This is useful for verifying the contents of the published package without actually publishing the package.

```sh
$ bun publish --dry-run
```

### `--auth-type`

If you have 2FA enabled for your npm account, `bun publish` will prompt you for a one-time password. This can be done through a browser or the CLI. The `--auth-type` flag can be used to tell the npm registry which method you prefer. The possible values are `web` and `legacy`, with `web` being the default.

```sh
$ bun publish --auth-type legacy
...
This operation requires a one-time password.
Enter OTP: 123456
...
```

### `--otp`

Provide a one-time password directly to the CLI. If the password is valid, this will skip the extra prompt for a one-time password before publishing. Example usage:

```sh
$ bun publish --otp 123456
```

### `--gzip-level`

Specify the level of gzip compression to use when packing the package. Only applies to `bun publish` without a tarball path argument. Values range from `0` to `9` (default is `9`).
3 changes: 3 additions & 0 deletions docs/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ export default {
page("cli/update", "`bun update`", {
description: "Update your project's dependencies.",
}),
page("cli/publish", "`bun publish`", {
description: "Publish your package to an npm registry.",
}),
page("cli/outdated", "`bun outdated`", {
description: "Check for outdated dependencies.",
}),
Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/bindings/bindings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,7 @@ pub const FetchHeaders = opaque {
pub fn createFromPicoHeaders(
pico_headers: anytype,
) *FetchHeaders {
const out = PicoHeaders{ .ptr = pico_headers.ptr, .len = pico_headers.len };
const out = PicoHeaders{ .ptr = pico_headers.list.ptr, .len = pico_headers.list.len };
const result = shim.cppFn("createFromPicoHeaders_", .{
&out,
});
Expand Down
2 changes: 1 addition & 1 deletion src/cli/create_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1996,7 +1996,7 @@ pub const Example = struct {

var is_expected_content_type = false;
var content_type: string = "";
for (response.headers) |header| {
for (response.headers.list) |header| {
if (strings.eqlCaseInsensitiveASCII(header.name, "content-type", true)) {
content_type = header.value;

Expand Down
93 changes: 59 additions & 34 deletions src/cli/publish_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -561,14 +561,7 @@ pub const PublishCommand = struct {
const prompt_for_otp = prompt_for_otp: {
if (res.status_code != 401) break :prompt_for_otp false;

if (authenticate: {
for (res.headers) |header| {
if (strings.eqlCaseInsensitiveASCII(header.name, "www-authenticate", true)) {
break :authenticate header.value;
}
}
break :authenticate null;
}) |@"www-authenticate"| {
if (res.headers.get("www-authenticate")) |@"www-authenticate"| {
var iter = strings.split(@"www-authenticate", ",");
while (iter.next()) |part| {
const trimmed = strings.trim(part, &strings.whitespace_chars);
Expand All @@ -583,7 +576,7 @@ pub const PublishCommand = struct {
Output.errGeneric("unable to authenticate, need: {s}", .{@"www-authenticate"});
Global.crash();
} else if (strings.containsComptime(response_buf.list.items, "one-time pass")) {
// missing www-authenticate header but one-time pass is still included
// missing www-authenicate header but one-time pass is still included
break :prompt_for_otp true;
}

Expand All @@ -592,7 +585,14 @@ pub const PublishCommand = struct {

if (!prompt_for_otp) {
// general error
return handleResponseErrors(directory_publish, ctx, &req, &res, &response_buf, true);
const otp_response = false;
return handleResponseErrors(directory_publish, ctx, &req, &res, &response_buf, otp_response);
}

if (res.headers.get("npm-notice")) |notice| {
Output.printError("\n", .{});
Output.note("{s}", .{notice});
Output.flush();
}

const otp = try getOTP(directory_publish, ctx, registry, &response_buf, &print_buf);
Expand Down Expand Up @@ -634,9 +634,16 @@ pub const PublishCommand = struct {

switch (otp_res.status_code) {
400...std.math.maxInt(@TypeOf(otp_res.status_code)) => {
return handleResponseErrors(directory_publish, ctx, &otp_req, &otp_res, &response_buf, true);
const otp_response = true;
return handleResponseErrors(directory_publish, ctx, &otp_req, &otp_res, &response_buf, otp_response);
},
else => {
if (otp_res.headers.get("npm-notice")) |notice| {
Output.printError("\n", .{});
Output.note("{s}", .{notice});
Output.flush();
}
},
else => {},
}
},
else => {},
Expand All @@ -649,7 +656,7 @@ pub const PublishCommand = struct {
req: *const http.AsyncHTTP,
res: *const bun.picohttp.Response,
response_body: *MutableString,
comptime check_for_success: bool,
comptime otp_response: bool,
) OOM!void {
const message = message: {
const source = logger.Source.initPathString("???", response_body.list.items);
Expand All @@ -660,29 +667,40 @@ pub const PublishCommand = struct {
}
};

if (comptime check_for_success) {
if (json.get("success")) |success_expr| {
if (success_expr.asBool()) |successful| {
if (successful) {
// possible to hit this with otp responses
return;
}
}
}
}
// I don't think we should make this check, I cannot find code in npm
// that does this
// if (comptime otp_response) {
// if (json.get("success")) |success_expr| {
// if (success_expr.asBool()) |successful| {
// if (successful) {
// // possible to hit this with otp responses
// return;
// }
// }
// }
// }

const @"error", _ = try json.getString(ctx.allocator, "error") orelse break :message null;
break :message @"error";
};

Output.prettyErrorln("\n<red>{d}<r>{s}{s}: {s}\n{s}{s}", .{
Output.prettyErrorln("\n<red>{d}<r>{s}{s}: {s}\n", .{
res.status_code,
if (res.status.len > 0) " " else "",
res.status,
bun.fmt.redactedNpmUrl(req.url.href),
if (message != null) "\n - " else "",
message orelse "",
});

if (message) |msg| {
if (comptime otp_response) {
if (res.status_code == 401 and strings.containsComptime(msg, "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.")) {
Output.prettyErrorln("\n - Received invalid OTP", .{});
Global.crash();
}
}
Output.prettyErrorln("\n - {s}", .{msg});
}

Global.crash();
}

Expand Down Expand Up @@ -810,12 +828,10 @@ pub const PublishCommand = struct {
202 => {
// retry
const nanoseconds = nanoseconds: {
default: for (res.headers) |header| {
if (strings.eqlCaseInsensitiveASCII(header.name, "retry-after", true)) {
const trimmed = strings.trim(header.value, &strings.whitespace_chars);
const seconds = bun.fmt.parseInt(u32, trimmed, 10) catch break :default;
break :nanoseconds seconds * std.time.ns_per_s;
}
if (res.headers.get("retry-after")) |retry| default: {
const trimmed = strings.trim(retry, &strings.whitespace_chars);
const seconds = bun.fmt.parseInt(u32, trimmed, 10) catch break :default;
break :nanoseconds seconds * std.time.ns_per_s;
}

break :nanoseconds 500 * std.time.ns_per_ms;
Expand All @@ -837,13 +853,22 @@ pub const PublishCommand = struct {
}
};

return try otp_done_json.getStringCloned(ctx.allocator, "token") orelse {
const token = try otp_done_json.getStringCloned(ctx.allocator, "token") orelse {
Output.err("WebLogin", "missing `token` field in reponse json", .{});
Global.crash();
};

if (res.headers.get("npm-notice")) |notice| {
Output.printError("\n", .{});
Output.note("{s}", .{notice});
Output.flush();
}

return token;
},
else => {
try handleResponseErrors(directory_publish, ctx, &req, &res, response_buf, false);
const otp_response = false;
try handleResponseErrors(directory_publish, ctx, &req, &res, response_buf, otp_response);
},
}
}
Expand Down
27 changes: 21 additions & 6 deletions src/deps/picohttp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const Match = ExactSizeMatcher(2);
const Output = bun.Output;
const Environment = bun.Environment;
const StringBuilder = bun.StringBuilder;
const string = bun.string;
const strings = bun.strings;

const fmt = std.fmt;

Expand All @@ -15,6 +17,19 @@ pub const Header = struct {
name: []const u8,
value: []const u8,

pub const List = struct {
list: []Header = &.{},

pub fn get(this: *const List, name: string) ?string {
for (this.list) |header| {
if (strings.eqlCaseInsensitiveASCII(header.name, name, true)) {
return header.value;
}
}
return null;
}
};

pub fn isMultiline(self: Header) bool {
return @intFromPtr(self.name.ptr) == 0;
}
Expand Down Expand Up @@ -216,7 +231,7 @@ pub const Response = struct {
minor_version: usize = 0,
status_code: u32 = 0,
status: []const u8 = "",
headers: []Header = &.{},
headers: Header.List = .{},
bytes_read: c_int = 0,

pub fn format(self: Response, comptime _: []const u8, _: fmt.FormatOptions, writer: anytype) !void {
Expand All @@ -234,7 +249,7 @@ pub const Response = struct {
self.status,
},
);
for (self.headers) |header| {
for (self.headers.list) |header| {
if (Output.enable_ansi_colors_stderr) {
_ = try writer.write(Output.prettyFmt("<r><d>[fetch]<r> ", true));
}
Expand All @@ -247,7 +262,7 @@ pub const Response = struct {
pub fn count(this: *const Response, builder: *StringBuilder) void {
builder.count(this.status);

for (this.headers) |header| {
for (this.headers.list) |header| {
header.count(builder);
}
}
Expand All @@ -256,11 +271,11 @@ pub const Response = struct {
var that = this.*;
that.status = builder.append(this.status);

for (this.headers, 0..) |header, i| {
for (this.headers.list, 0..) |header, i| {
headers[i] = header.clone(builder);
}

that.headers = headers[0..this.headers.len];
that.headers.list = headers[0..this.headers.list.len];

return that;
}
Expand Down Expand Up @@ -297,7 +312,7 @@ pub const Response = struct {
.minor_version = @as(usize, @intCast(minor_version)),
.status_code = @as(u32, @intCast(status_code)),
.status = status,
.headers = src[0..@min(num_headers, src.len)],
.headers = .{ .list = src[0..@min(num_headers, src.len)] },
.bytes_read = rc,
},
};
Expand Down
Loading

0 comments on commit 39b1c01

Please sign in to comment.