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 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 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(1)
391 .map(|s| s.trim().trim_start_matches('-').trim())
392 .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}