xtask/
publish.rs

1use std::{env, fs, path::PathBuf, process, str::FromStr};
2
3use owo_colors::OwoColorize;
4use rustc_hash::{FxHashMap, FxHashSet};
5use semver::{Prerelease, Version};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::command::Command;
10
11const PLATFORM_LINUX_X64: NpmSupportedPlatform = NpmSupportedPlatform {
12    os: "linux",
13    arch: "x64",
14    rust_target: "x86_64-unknown-linux-musl",
15};
16
17const PLATFORM_DARWIN_X64: NpmSupportedPlatform = NpmSupportedPlatform {
18    os: "darwin",
19    arch: "x64",
20    rust_target: "x86_64-apple-darwin",
21};
22
23const PLATFORM_DARWIN_ARM64: NpmSupportedPlatform = NpmSupportedPlatform {
24    os: "darwin",
25    arch: "arm64",
26    rust_target: "aarch64-apple-darwin",
27};
28
29const PLATFORM_WIN32_X64: NpmSupportedPlatform = NpmSupportedPlatform {
30    os: "win32",
31    arch: "x64",
32    rust_target: "x86_64-pc-windows-msvc",
33};
34
35const NPM_PACKAGES: &[NpmPackage] = &[NpmPackage {
36    crate_name: "node-file-trace",
37    name: "@vercel/experimental-nft",
38    description: "Node.js module trace",
39    bin: "node-file-trace",
40    platform: &[
41        PLATFORM_LINUX_X64,
42        PLATFORM_DARWIN_X64,
43        PLATFORM_DARWIN_ARM64,
44        PLATFORM_WIN32_X64,
45    ],
46}];
47
48struct NpmSupportedPlatform {
49    os: &'static str,
50    arch: &'static str,
51    rust_target: &'static str,
52}
53
54struct NpmPackage {
55    crate_name: &'static str,
56    name: &'static str,
57    description: &'static str,
58    bin: &'static str,
59    platform: &'static [NpmSupportedPlatform],
60}
61
62pub fn run_publish(name: &str) {
63    if let Some(pkg) = NPM_PACKAGES.iter().find(|p| p.crate_name == name) {
64        let mut optional_dependencies = Vec::with_capacity(pkg.platform.len());
65        let mut is_alpha = false;
66        let mut is_beta = false;
67        let mut is_canary = false;
68        let version = if let Ok(release_version) = env::var("RELEASE_VERSION") {
69            // node-file-trace@1.0.0-alpha.1
70            let release_tag_version = release_version
71                .trim()
72                .trim_start_matches("node-file-trace@");
73            if let Ok(semver_version) = Version::parse(release_tag_version) {
74                is_alpha = semver_version.pre.contains("alpha");
75                is_beta = semver_version.pre.contains("beta");
76                is_canary = semver_version.pre.contains("canary");
77            };
78            release_tag_version.to_owned()
79        } else {
80            format!(
81                "0.0.0-{}",
82                env::var("GITHUB_SHA")
83                    .map(|mut sha| {
84                        sha.truncate(7);
85                        sha
86                    })
87                    .unwrap_or_else(|_| {
88                        if let Ok(mut o) = process::Command::new("git")
89                            .args(["rev-parse", "--short", "HEAD"])
90                            .output()
91                            .map(|o| String::from_utf8(o.stdout).expect("Invalid utf8 output"))
92                        {
93                            o.truncate(7);
94                            return o;
95                        }
96                        panic!("Unable to get git commit sha");
97                    })
98            )
99        };
100        let tag = if is_alpha {
101            "alpha"
102        } else if is_beta {
103            "beta"
104        } else if is_canary {
105            "canary"
106        } else {
107            "latest"
108        };
109        let current_dir = env::current_dir().expect("Unable to get current directory");
110        let package_dir = current_dir.join("../../packages").join("node-module-trace");
111        let temp_dir = package_dir.join("npm");
112        if let Ok(()) = fs::remove_dir_all(&temp_dir) {};
113        fs::create_dir(&temp_dir).expect("Unable to create temporary npm directory");
114        for platform in pkg.platform.iter() {
115            let bin_file_name = if platform.os == "win32" {
116                format!("{}.exe", pkg.bin)
117            } else {
118                pkg.bin.to_string()
119            };
120            let platform_package_name = format!("{}-{}-{}", pkg.name, platform.os, platform.arch);
121            optional_dependencies.push(platform_package_name.clone());
122            let pkg_json = serde_json::json!({
123              "name": platform_package_name,
124              "version": version,
125              "description": pkg.description,
126              "os": [platform.os],
127              "cpu": [platform.arch],
128              "bin": {
129                pkg.bin: bin_file_name
130              }
131            });
132            let dir_name = format!("{}-{}-{}", pkg.crate_name, platform.os, platform.arch);
133            let target_dir = package_dir.join("npm").join(dir_name);
134            fs::create_dir(&target_dir)
135                .unwrap_or_else(|e| panic!("Unable to create dir: {:?}\n{e}", &target_dir));
136            fs::write(
137                target_dir.join("../../package.json"),
138                serde_json::to_string_pretty(&pkg_json).unwrap(),
139            )
140            .expect("Unable to write package.json");
141            let artifact_path = current_dir
142                .join("artifacts")
143                .join(format!("node-file-trace-{}", platform.rust_target))
144                .join(&bin_file_name);
145            let dist_path = target_dir.join(&bin_file_name);
146            fs::copy(&artifact_path, &dist_path).unwrap_or_else(|e| {
147                panic!("Copy file from [{artifact_path:?}] to [{dist_path:?}] failed: {e}")
148            });
149            Command::program("npm")
150                .args(["publish", "--access", "public", "--tag", tag])
151                .error_message("Publish npm package failed")
152                .current_dir(target_dir)
153                .execute();
154        }
155        let target_pkg_dir = temp_dir.join(pkg.name);
156        fs::create_dir_all(&target_pkg_dir).unwrap_or_else(|e| {
157            panic!("Unable to create target npm directory [{target_pkg_dir:?}]: {e}")
158        });
159        let optional_dependencies_with_version = optional_dependencies
160            .into_iter()
161            .map(|name| (name, version.clone()))
162            .collect::<FxHashMap<String, String>>();
163        let pkg_json_content =
164            fs::read(package_dir.join("../../package.json")).expect("Unable to read package.json");
165        let mut pkg_json: Value = serde_json::from_slice(&pkg_json_content).unwrap();
166        pkg_json["optionalDependencies"] =
167            serde_json::to_value(optional_dependencies_with_version).unwrap();
168        fs::write(
169            target_pkg_dir.join("../../package.json"),
170            serde_json::to_string_pretty(&pkg_json).unwrap(),
171        )
172        .unwrap_or_else(|e| {
173            panic!(
174                "Write [{:?}] failed: {e}",
175                target_pkg_dir.join("../../package.json")
176            )
177        });
178        Command::program("npm")
179            .args(["publish", "--access", "public", "--tag", tag])
180            .error_message("Publish npm package failed")
181            .current_dir(target_pkg_dir)
182            .execute();
183    }
184}
185
186const VERSION_TYPE: &[&str] = &["patch", "minor", "major", "alpha", "beta", "canary"];
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189struct WorkspaceProjectMeta {
190    #[serde(default = "default_empty_string")]
191    name: String,
192    path: String,
193    private: bool,
194}
195
196fn default_empty_string() -> String {
197    String::new()
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201struct PackageJson {
202    #[serde(default = "default_empty_string")]
203    version: String,
204    #[serde(default = "default_empty_string")]
205    name: String,
206    #[serde(default)]
207    private: bool,
208    alias: Option<String>,
209    #[serde(default = "default_empty_string")]
210    path: String,
211}
212
213pub fn run_bump(names: FxHashSet<String>, dry_run: bool) {
214    let workspaces_list_text = Command::program("pnpm")
215        .args(["ls", "-r", "--depth", "-1", "--json"])
216        .error_message("List workspaces failed")
217        .output_string();
218    let workspaces = serde_json::from_str::<Vec<WorkspaceProjectMeta>>(workspaces_list_text.trim())
219        .expect("Unable to parse workspaces list")
220        .iter()
221        .filter_map(|workspace| {
222            let workspace_pkg_json = fs::read_to_string(
223                env::current_dir()
224                    .unwrap()
225                    .join(&workspace.path)
226                    .join("package.json"),
227            )
228            .expect("Read workspace package.json failed");
229            let mut pkg_json: PackageJson = serde_json::from_str(&workspace_pkg_json)
230                .expect("Parse workspace package.json failed");
231            if workspace.name.is_empty() || pkg_json.private {
232                None
233            } else {
234                pkg_json.path.clone_from(&workspace.path);
235                Some(pkg_json)
236            }
237        })
238        .collect::<Vec<PackageJson>>();
239    let mut workspaces_to_bump = workspaces
240        .iter()
241        .filter(|&p| names.contains(&p.name))
242        .cloned()
243        .collect::<Vec<_>>();
244    if workspaces_to_bump.is_empty() {
245        fn name_to_title(package: &PackageJson) -> String {
246            format!(
247                "{}, current version is {}",
248                package.name.bright_cyan(),
249                package.version.bright_green()
250            )
251        }
252        let selector = inquire::MultiSelect::new(
253            "Select a package to bump",
254            workspaces.iter().map(name_to_title).collect(),
255        );
256        workspaces_to_bump = selector
257            .prompt()
258            .expect("Failed to prompt packages")
259            .iter()
260            .filter_map(|p| workspaces.iter().find(|w| name_to_title(w) == *p))
261            .cloned()
262            .collect();
263    }
264    let mut tags_to_apply = Vec::new();
265    workspaces_to_bump.iter().for_each(|p| {
266        let title = format!("Version for {}", &p.name);
267        let selector = inquire::Select::new(title.as_str(), VERSION_TYPE.to_owned());
268        let version_type = selector.prompt().expect("Get version type failed");
269        let mut semver_version = Version::parse(&p.version).unwrap_or_else(|e| {
270            panic!("Failed to parse {} in {} as semver: {e}", p.version, p.name)
271        });
272        match version_type {
273            "major" => {
274                semver_version.major += 1;
275                semver_version.minor = 0;
276                semver_version.patch = 0;
277                semver_version.pre = Prerelease::EMPTY;
278            }
279            "minor" => {
280                semver_version.minor += 1;
281                semver_version.patch = 0;
282                semver_version.pre = Prerelease::EMPTY;
283            }
284            "patch" => {
285                semver_version.patch += 1;
286                semver_version.pre = Prerelease::EMPTY;
287            }
288            "alpha" | "beta" | "canary" => {
289                if semver_version.pre.is_empty() {
290                    semver_version.patch += 1;
291                    semver_version.pre =
292                        Prerelease::new(format!("{version_type}.0").as_str()).unwrap();
293                } else {
294                    let mut prerelease_version = semver_version.pre.split('.');
295                    let prerelease_type = prerelease_version
296                        .next()
297                        .expect("prerelease type should exist");
298                    let prerelease_version = prerelease_version
299                        .next()
300                        .expect("prerelease version number should exist");
301                    let mut version_number = prerelease_version
302                        .parse::<u32>()
303                        .expect("prerelease version number should be u32");
304                    if semver_version.pre.contains(version_type) {
305                        version_number += 1;
306                        semver_version.pre =
307                            Prerelease::new(format!("{version_type}.{version_number}").as_str())
308                                .unwrap();
309                    } else {
310                        // eg. current version is 1.0.0-beta.12, bump to 1.0.0-canary.0
311                        if Prerelease::from_str(version_type).unwrap()
312                            > Prerelease::from_str(prerelease_type).unwrap()
313                        {
314                            semver_version.pre =
315                                Prerelease::new(format!("{version_type}.0").as_str()).unwrap();
316                        } else {
317                            panic!(
318                                "Previous version is {prerelease_type}, so you can't bump to \
319                                 {version_type}",
320                            );
321                        }
322                    }
323                }
324            }
325            _ => unreachable!(),
326        }
327        let semver_version_string = semver_version.to_string();
328        let version_command_args = vec![
329            "version",
330            semver_version_string.as_str(),
331            "--no-git-tag-version",
332            "--no-commit-hooks",
333        ];
334        Command::program("pnpm")
335            .args(version_command_args)
336            .current_dir(PathBuf::from(&p.path))
337            .dry_run(dry_run)
338            .error_message("Bump version failed")
339            .execute();
340        tags_to_apply.push(format!(
341            "{}@{}",
342            p.alias.as_ref().unwrap_or(&p.name),
343            semver_version_string
344        ));
345    });
346    Command::program("pnpm")
347        .args(["install"])
348        .dry_run(dry_run)
349        .error_message("Update pnpm-lock.yaml failed")
350        .execute();
351    Command::program("git")
352        .args(["add", "."])
353        .dry_run(dry_run)
354        .error_message("Stash git changes failed")
355        .execute();
356    let tags_message = tags_to_apply
357        .iter()
358        .map(|s| format!("- {s}"))
359        .collect::<Vec<_>>()
360        .join("\n");
361    Command::program("git")
362        .args([
363            "commit",
364            "-m",
365            "chore: release turbopack npm packages",
366            "-m",
367            tags_message.as_str(),
368        ])
369        .dry_run(dry_run)
370        .error_message("Stash git changes failed")
371        .execute();
372    for tag in tags_to_apply {
373        Command::program("git")
374            .dry_run(dry_run)
375            .args(["tag", "-s", &tag, "-m", &tag])
376            .error_message("Tag failed")
377            .execute();
378    }
379}
380
381pub fn publish_workspace(dry_run: bool) {
382    let commit_message = Command::program("git")
383        .args(["log", "-1", "--pretty=%B"])
384        .error_message("Get commit hash failed")
385        .output_string();
386    for (pkg_name_without_scope, version) in commit_message
387        .trim()
388        .split('\n')
389        // Skip commit title
390        .skip(1)
391        .map(|s| s.trim().trim_start_matches('-').trim())
392        // Only publish tags match `@vercel/xxx@x.y.z-alpha.n`
393        .filter(|m| m.starts_with("@vercel/"))
394        .map(|m| {
395            let m = m.trim_start_matches("@vercel/");
396            let mut full_tag = m.split('@');
397            let pkg_name_without_scope = full_tag.next().unwrap().to_string();
398            let version = full_tag.next().unwrap().to_string();
399            (pkg_name_without_scope, version)
400        })
401    {
402        let pkg_name = format!("@vercel/{pkg_name_without_scope}");
403        let semver_version = Version::from_str(version.as_str())
404            .unwrap_or_else(|e| panic!("Parse semver version failed {version} {e}"));
405        let is_alpha = semver_version.pre.contains("alpha");
406        let is_beta = semver_version.pre.contains("beta");
407        let is_canary = semver_version.pre.contains("canary");
408        let tag = {
409            if is_alpha {
410                "alpha"
411            } else if is_beta {
412                "beta"
413            } else if is_canary {
414                "canary"
415            } else {
416                "latest"
417            }
418        };
419        let mut args = vec![
420            "publish",
421            "--tag",
422            tag,
423            "--no-git-checks",
424            "--filter",
425            pkg_name.as_str(),
426        ];
427        if dry_run {
428            args.push("--dry-run");
429        }
430        Command::program("pnpm")
431            .args(args)
432            .error_message("Publish failed")
433            .execute();
434    }
435}