@fuman/build 0.0.8 → 0.0.11

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.
@@ -17,7 +17,11 @@ function formatBumpVersionResult(result, withReleaseType) {
17
17
  lines.push("");
18
18
  lines.push("list of changed packages:");
19
19
  for (const { package: pkg, because, prevVersion } of result.changedPackages) {
20
- lines.push(` ${pkg.json.name}${because ? ` (because of ${because.join(", ")})` : ""}: ${prevVersion} → ${pkg.json.version}`);
20
+ let versionStr = prevVersion;
21
+ if (!pkg.json.fuman?.ownVersioning) {
22
+ versionStr += ` → ${pkg.json.version}`;
23
+ }
24
+ lines.push(` ${pkg.json.name}${because ? ` (because of ${because.join(", ")})` : ""}: ${versionStr}`);
21
25
  }
22
26
  return lines.join("\n");
23
27
  }
@@ -7,7 +7,7 @@ import { createGithubRelease } from "../../git/github.js";
7
7
  import { getLatestTag, getFirstCommit, gitTagExists } from "../../git/utils.js";
8
8
  import { jsrCreatePackages } from "../../jsr/create-packages.js";
9
9
  import { generateDenoWorkspace } from "../../jsr/generate-workspace.js";
10
- import { exec } from "../../misc/exec.js";
10
+ import { exec, ExecError } from "../../misc/exec.js";
11
11
  import { collectPackageJsons } from "../../package-json/collect-package-jsons.js";
12
12
  import { bumpVersion } from "../../versioning/bump-version.js";
13
13
  import { generateChangelog } from "../../versioning/generate-changelog.js";
@@ -187,6 +187,7 @@ const releaseCli = bc.command({
187
187
  if (args.dryRun) {
188
188
  console.log("dry run, skipping release commit and tag");
189
189
  } else {
190
+ await config?.versioning?.beforeReleaseCommit?.(workspaceWithRoot);
190
191
  let message = `chore(release): ${tagName}`;
191
192
  if (!args.withGithubRelease) {
192
193
  message += `
@@ -217,11 +218,19 @@ ${changelog}`;
217
218
  stdio: "inherit",
218
219
  throwOnError: true
219
220
  });
220
- await exec(["git", "push", origin, "--force", "--tags"], {
221
- cwd: root,
222
- stdio: "inherit",
223
- throwOnError: true
224
- });
221
+ try {
222
+ await exec(["git", "push", origin, "--force", "--tags"], {
223
+ cwd: root,
224
+ throwOnError: true
225
+ });
226
+ } catch (e) {
227
+ if (!(e instanceof ExecError)) throw e;
228
+ if (e.result.stderr.includes(`cannot lock ref 'refs/tags/${tagName}': reference already exists`)) {
229
+ console.log(`❗ tag ${tagName} already exists on ${origin}, skipping`);
230
+ } else {
231
+ throw e;
232
+ }
233
+ }
225
234
  }
226
235
  }
227
236
  }
@@ -32,13 +32,19 @@ export declare function validateWorkspaceDeps(params: {
32
32
  * @default true
33
33
  */
34
34
  skipWorkspaceDeps?: boolean;
35
+ /** whether to skip validating peer dependencies */
36
+ skipPeerDeps?: boolean;
35
37
  }): Promise<WorkspaceDepsError[]>;
36
38
  export declare const validateWorkspaceDepsCli: bc.Command<{
37
39
  includeRoot: boolean;
38
40
  noSkipWorkspaceDeps: boolean | undefined;
39
41
  root: string | undefined;
42
+ withErrorCode: boolean | undefined;
43
+ skipPeerDeps: boolean | undefined;
40
44
  }, {
41
45
  includeRoot: boolean;
42
46
  noSkipWorkspaceDeps: boolean | undefined;
43
47
  root: string | undefined;
48
+ withErrorCode: boolean | undefined;
49
+ skipPeerDeps: boolean | undefined;
44
50
  }>;
@@ -6,7 +6,8 @@ async function validateWorkspaceDeps(params) {
6
6
  const {
7
7
  workspaceRoot,
8
8
  includeRoot = true,
9
- skipWorkspaceDeps = true
9
+ skipWorkspaceDeps = true,
10
+ skipPeerDeps = true
10
11
  } = params;
11
12
  const pjs = await collectPackageJsons(workspaceRoot, includeRoot);
12
13
  const workspacePackages = new Set(skipWorkspaceDeps ? pjs.map((pj) => pj.json.name) : []);
@@ -19,6 +20,7 @@ async function validateWorkspaceDeps(params) {
19
20
  for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
20
21
  const deps = pj[field];
21
22
  if (!deps) continue;
23
+ if (field === "peerDependencies" && skipPeerDeps) continue;
22
24
  for (const [name, version] of Object.entries(deps)) {
23
25
  if (workspacePackages.has(name)) continue;
24
26
  if (version.startsWith("workspace:")) continue;
@@ -26,7 +28,13 @@ async function validateWorkspaceDeps(params) {
26
28
  versions[name] = {};
27
29
  }
28
30
  for (const [pkgName, pkgDepVersions] of Object.entries(versions[name])) {
29
- if (!satisfies(version, pkgDepVersions)) {
31
+ let ok = true;
32
+ if (pkgDepVersions.match(/^(?:https?:\/\/|catalog:)/)) {
33
+ ok = version === pkgDepVersions;
34
+ } else {
35
+ ok = satisfies(version, pkgDepVersions);
36
+ }
37
+ if (!ok) {
30
38
  errors.push({
31
39
  package: pj.name,
32
40
  dependency: name,
@@ -49,19 +57,25 @@ const validateWorkspaceDepsCli = bc.command({
49
57
  options: {
50
58
  includeRoot: bc.boolean("include-root").desc("whether to also validate the root package.json").default(false),
51
59
  noSkipWorkspaceDeps: bc.boolean("no-skip-workspace-deps").desc("whether to not skip validating dependencies of other workspace packages"),
52
- root: bc.string().desc("path to the root of the workspace (default: cwd)")
60
+ root: bc.string().desc("path to the root of the workspace (default: cwd)"),
61
+ withErrorCode: bc.boolean("with-error-code").desc("whether to exit with a non-zero code if there are mismatches"),
62
+ skipPeerDeps: bc.boolean("skip-peer-deps").desc("whether to skip validating peer dependencies")
53
63
  },
54
64
  handler: async (args) => {
55
65
  const errors = await validateWorkspaceDeps({
56
66
  workspaceRoot: args.root ?? process.cwd(),
57
67
  includeRoot: args.includeRoot,
58
- skipWorkspaceDeps: !args.noSkipWorkspaceDeps
68
+ skipWorkspaceDeps: !args.noSkipWorkspaceDeps,
69
+ skipPeerDeps: args.skipPeerDeps
59
70
  });
60
71
  if (errors.length > 0) {
61
72
  console.error("⚠️ Found external dependencies mismatch:");
62
73
  for (const error of errors) {
63
74
  console.error(` - at ${error.package}: ${error.at} has ${error.dependency}@${error.version}, but ${error.otherPackage} has @${error.otherVersion}`);
64
75
  }
76
+ if (args.withErrorCode) {
77
+ process.exit(1);
78
+ }
65
79
  } else {
66
80
  console.log("✅ All external dependencies match!");
67
81
  }
package/index.js CHANGED
@@ -4,20 +4,21 @@ import { generateDocs } from "./cli/commands/docs.js";
4
4
  import { generateDepsGraph } from "./cli/commands/gen-deps-graph.js";
5
5
  import { validateWorkspaceDeps } from "./cli/commands/validate-workspace-deps.js";
6
6
  import { findChangedFiles, getCommitsBetween, getCurrentBranch, getCurrentCommit, getFirstCommit, getLatestTag, gitTagExists, parseConventionalCommit } from "./git/utils.js";
7
- import { exec } from "./misc/exec.js";
7
+ import { ExecError, exec } from "./misc/exec.js";
8
8
  import { normalizeFilePath } from "./misc/path.js";
9
9
  import { determinePublishOrder, sortWorkspaceByPublishOrder } from "./misc/publish-order.js";
10
10
  import { getTsconfigFiles, getTsconfigFor } from "./misc/tsconfig.js";
11
11
  import { NPM_PACKAGE_NAME_REGEX, npmCheckVersion } from "./npm/npm-api.js";
12
12
  import { collectPackageJsons, filterPackageJsonsForPublish } from "./package-json/collect-package-jsons.js";
13
13
  import { findPackageJson } from "./package-json/find-package-json.js";
14
- import { parsePackageJson, parsePackageJsonFile, parseWorkspaceRootPackageJson } from "./package-json/parse.js";
14
+ import { parsePackageJson, parsePackageJsonFile, parsePackageJsonFromDir, parseWorkspaceRootPackageJson } from "./package-json/parse.js";
15
15
  import { processPackageJson } from "./package-json/process-package-json.js";
16
16
  import { PackageJsonSchema } from "./package-json/types.js";
17
17
  import { bumpVersion } from "./versioning/bump-version.js";
18
18
  import { findProjectChangedFiles, findProjectChangedPackages } from "./versioning/collect-files.js";
19
19
  import { generateChangelog } from "./versioning/generate-changelog.js";
20
20
  export {
21
+ ExecError,
21
22
  NPM_PACKAGE_NAME_REGEX,
22
23
  PackageJsonSchema,
23
24
  buildPackage,
@@ -48,6 +49,7 @@ export {
48
49
  parseConventionalCommit,
49
50
  parsePackageJson,
50
51
  parsePackageJsonFile,
52
+ parsePackageJsonFromDir,
51
53
  parseWorkspaceRootPackageJson,
52
54
  processPackageJson,
53
55
  sortWorkspaceByPublishOrder,
package/misc/exec.d.ts CHANGED
@@ -8,12 +8,17 @@ export interface ExecResult {
8
8
  /** exit code of the command */
9
9
  exitCode: number;
10
10
  }
11
+ export declare class ExecError extends Error {
12
+ readonly cmd: string[];
13
+ readonly result: ExecResult;
14
+ constructor(cmd: string[], result: ExecResult);
15
+ }
11
16
  /**
12
17
  * execute a command and return its result
13
18
  *
14
19
  * **differences from node's `child_process.exec()`**:
15
20
  * - if `options.stdio` is set to `'inherit'`, the command will be printed to the console (unless `options.quiet` is set to `true`)
16
- * - on non-zero exit code, the promise will be rejected with an error if `options.throwOnError` is set to `true`
21
+ * - on non-zero exit code, the promise will be rejected with an {@link ExecError} if `options.throwOnError` is set to `true`
17
22
  *
18
23
  * @param cmd command to execute (first element is the command itself, the rest are arguments to it)
19
24
  */
package/misc/exec.js CHANGED
@@ -2,6 +2,15 @@ import path__default from "node:path";
2
2
  import process from "node:process";
3
3
  import { spawn } from "cross-spawn";
4
4
  import { normalizeFilePath } from "./path.js";
5
+ class ExecError extends Error {
6
+ constructor(cmd, result) {
7
+ super(`Command exited with code ${result.exitCode}`, {
8
+ cause: result
9
+ });
10
+ this.cmd = cmd;
11
+ this.result = result;
12
+ }
13
+ }
5
14
  function exec(cmd, options) {
6
15
  return new Promise((resolve, reject) => {
7
16
  if (options?.stdio === "inherit" && !options.quiet) {
@@ -29,12 +38,10 @@ function exec(cmd, options) {
29
38
  proc.on("error", reject);
30
39
  proc.on("close", (code) => {
31
40
  if (code !== 0 && options?.throwOnError) {
32
- reject(new Error(`Command exited with code ${code}`, {
33
- cause: {
34
- stderr: Buffer.concat(stderr).toString(),
35
- exitCode: code,
36
- cmd
37
- }
41
+ reject(new ExecError(cmd, {
42
+ stdout: Buffer.concat(stdout).toString(),
43
+ stderr: Buffer.concat(stderr).toString(),
44
+ exitCode: code ?? -1
38
45
  }));
39
46
  }
40
47
  resolve({
@@ -46,5 +53,6 @@ function exec(cmd, options) {
46
53
  });
47
54
  }
48
55
  export {
56
+ ExecError,
49
57
  exec
50
58
  };
@@ -3,6 +3,8 @@ import { PackageJson } from './types.js';
3
3
  export interface WorkspacePackage {
4
4
  /** path to the package root */
5
5
  path: string;
6
+ /** path to the package.json file (note that it might not be a .json file) */
7
+ packageJsonPath: string;
6
8
  /** whether this is the root package */
7
9
  root: boolean;
8
10
  /** package.json of the package */
@@ -2,12 +2,12 @@ import * as path from "node:path";
2
2
  import process from "node:process";
3
3
  import { glob } from "tinyglobby";
4
4
  import { normalizeFilePath } from "../misc/path.js";
5
- import { parseWorkspaceRootPackageJson, parsePackageJsonFile } from "./parse.js";
5
+ import { parseWorkspaceRootPackageJson, parsePackageJsonFromDir } from "./parse.js";
6
6
  const maxDepth = process.env.FUMAN_BUILD_MAX_DEPTH !== void 0 ? Number(process.env.FUMAN_BUILD_MAX_DEPTH) : 5;
7
7
  async function collectPackageJsons(workspaceRoot, includeRoot = false) {
8
8
  workspaceRoot = normalizeFilePath(workspaceRoot);
9
9
  const packageJsons = [];
10
- const rootPackageJson = await parseWorkspaceRootPackageJson(workspaceRoot);
10
+ const { path: rootPackageJsonPath, json: rootPackageJson } = await parseWorkspaceRootPackageJson(workspaceRoot);
11
11
  if (!rootPackageJson.workspaces) {
12
12
  throw new Error("No workspaces found in package.json");
13
13
  }
@@ -15,7 +15,8 @@ async function collectPackageJsons(workspaceRoot, includeRoot = false) {
15
15
  packageJsons.push({
16
16
  path: workspaceRoot,
17
17
  root: true,
18
- json: rootPackageJson
18
+ json: rootPackageJson,
19
+ packageJsonPath: rootPackageJsonPath
19
20
  });
20
21
  }
21
22
  for (const dir of await glob({
@@ -26,14 +27,15 @@ async function collectPackageJsons(workspaceRoot, includeRoot = false) {
26
27
  deep: maxDepth
27
28
  })) {
28
29
  try {
29
- const packageJson = await parsePackageJsonFile(path.join(workspaceRoot, dir, "package.json"));
30
+ const { json, path: packageJsonPath } = await parsePackageJsonFromDir(path.join(workspaceRoot, dir));
30
31
  packageJsons.push({
31
32
  path: path.join(workspaceRoot, dir),
32
33
  root: false,
33
- json: packageJson
34
+ packageJsonPath,
35
+ json
34
36
  });
35
37
  } catch (err) {
36
- if (err.code === "ENOENT") ;
38
+ if (err.code === "ENOENT" || err.cause?.notFound === true) ;
37
39
  else {
38
40
  throw err;
39
41
  }
@@ -1,7 +1,14 @@
1
1
  import { PackageJson } from './types.js';
2
2
  /** parse a package.json from string */
3
- export declare function parsePackageJson(packageJson: string): PackageJson;
3
+ export declare function parsePackageJson(packageJson: string, format?: 'json' | 'yaml'): PackageJson;
4
4
  /** parse a package.json file */
5
5
  export declare function parsePackageJsonFile(packageJsonPath: string | URL): Promise<PackageJson>;
6
+ export declare function parsePackageJsonFromDir(dir: string | URL): Promise<{
7
+ path: string;
8
+ json: PackageJson;
9
+ }>;
6
10
  /** parse the package.json file at the root of the workspace */
7
- export declare function parseWorkspaceRootPackageJson(workspaceRoot: string | URL): Promise<PackageJson>;
11
+ export declare function parseWorkspaceRootPackageJson(workspaceRoot: string | URL): Promise<{
12
+ path: string;
13
+ json: PackageJson;
14
+ }>;
@@ -1,18 +1,58 @@
1
1
  import * as fsp from "node:fs/promises";
2
- import path__default from "node:path";
2
+ import path__default, { extname } from "node:path";
3
3
  import * as jsyaml from "js-yaml";
4
+ import * as json5_ from "json5";
5
+ import { fileExists } from "../misc/fs.js";
4
6
  import { normalizeFilePath } from "../misc/path.js";
5
7
  import { PackageJsonSchema } from "./types.js";
6
- function parsePackageJson(packageJson) {
7
- return PackageJsonSchema.parse(JSON.parse(packageJson));
8
+ let json5 = json5_;
9
+ if ("default" in json5_) {
10
+ json5 = json5_.default;
11
+ }
12
+ function parsePackageJson(packageJson, format = "json") {
13
+ let obj;
14
+ if (format === "json") {
15
+ obj = json5.parse(packageJson);
16
+ } else {
17
+ obj = jsyaml.load(packageJson);
18
+ }
19
+ return PackageJsonSchema.parse(obj);
8
20
  }
9
21
  async function parsePackageJsonFile(packageJsonPath) {
10
- return parsePackageJson(await fsp.readFile(normalizeFilePath(packageJsonPath), "utf8"));
22
+ const path2 = normalizeFilePath(packageJsonPath);
23
+ const ext = extname(path2).slice(1);
24
+ let format;
25
+ if (ext === "json5" || ext === "jsonc" || ext === "json") format = "json";
26
+ else if (ext === "yml" || ext === "yaml") format = "yaml";
27
+ else throw new Error(`Unknown package.json extension: ${ext}`);
28
+ try {
29
+ return parsePackageJson(await fsp.readFile(normalizeFilePath(packageJsonPath), "utf8"), format);
30
+ } catch (err) {
31
+ throw new Error(`Could not parse package.json at ${packageJsonPath}`, { cause: err });
32
+ }
33
+ }
34
+ const EXT_OPTIONS = ["json", "json5", "jsonc", "yml", "yaml"];
35
+ async function parsePackageJsonFromDir(dir) {
36
+ dir = normalizeFilePath(dir);
37
+ let packageJsonPath;
38
+ for (const ext of EXT_OPTIONS) {
39
+ const tmp = path__default.join(dir, `package.${ext}`);
40
+ if (await fileExists(tmp)) {
41
+ packageJsonPath = tmp;
42
+ break;
43
+ }
44
+ }
45
+ if (packageJsonPath == null) {
46
+ throw new Error(`Could not find package.json at ${dir}`, { cause: { notFound: true } });
47
+ }
48
+ return {
49
+ path: packageJsonPath,
50
+ json: await parsePackageJsonFile(packageJsonPath)
51
+ };
11
52
  }
12
53
  async function parseWorkspaceRootPackageJson(workspaceRoot) {
13
54
  workspaceRoot = normalizeFilePath(workspaceRoot);
14
- const packageJsonPath = path__default.join(workspaceRoot, "package.json");
15
- const parsed = await parsePackageJsonFile(packageJsonPath);
55
+ const { path: pjPath, json: parsed } = await parsePackageJsonFromDir(workspaceRoot);
16
56
  if (!parsed.workspaces) {
17
57
  const pnpmWorkspacePath = path__default.join(workspaceRoot, "pnpm-workspace.yaml");
18
58
  let yaml;
@@ -20,7 +60,7 @@ async function parseWorkspaceRootPackageJson(workspaceRoot) {
20
60
  yaml = await fsp.readFile(pnpmWorkspacePath, "utf8");
21
61
  } catch (e) {
22
62
  if (e.code !== "ENOENT") throw e;
23
- return parsed;
63
+ return { path: pjPath, json: parsed };
24
64
  }
25
65
  const workspace = jsyaml.load(yaml);
26
66
  if (!workspace.packages) {
@@ -40,10 +80,11 @@ async function parseWorkspaceRootPackageJson(workspaceRoot) {
40
80
  }
41
81
  parsed.workspaces = workspace.packages;
42
82
  }
43
- return parsed;
83
+ return { path: pjPath, json: parsed };
44
84
  }
45
85
  export {
46
86
  parsePackageJson,
47
87
  parsePackageJsonFile,
88
+ parsePackageJsonFromDir,
48
89
  parseWorkspaceRootPackageJson
49
90
  };
@@ -34,7 +34,7 @@ const PackageJsonSchema = z.object({
34
34
  engines: z.record(z.string()),
35
35
  pnpm: z.object({
36
36
  overrides: z.record(z.string())
37
- }),
37
+ }).partial(),
38
38
  fuman: z.object({
39
39
  jsr: z.union([
40
40
  z.literal("skip"),
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "@fuman/build",
3
3
  "type": "module",
4
- "version": "0.0.8",
4
+ "version": "0.0.11",
5
5
  "description": "utils for building packages and managing monorepos",
6
6
  "license": "MIT",
7
7
  "scripts": {},
8
8
  "dependencies": {
9
9
  "@drizzle-team/brocli": "^0.10.2",
10
- "@fuman/fetch": "^0.0.8",
11
- "@fuman/io": "^0.0.8",
12
- "@fuman/node": "^0.0.8",
13
- "@fuman/utils": "^0.0.4",
10
+ "@fuman/fetch": "^0.0.11",
11
+ "@fuman/io": "^0.0.11",
12
+ "@fuman/node": "^0.0.11",
13
+ "@fuman/utils": "^0.0.11",
14
14
  "cross-spawn": "^7.0.5",
15
15
  "detect-indent": "^7.0.1",
16
16
  "js-yaml": "^4.1.0",
17
+ "json5": "^2.2.3",
17
18
  "picomatch": "^4.0.2",
18
19
  "semver": "^7.6.3",
19
20
  "tinyglobby": "^0.2.6",
@@ -1,5 +1,6 @@
1
1
  import { MaybePromise } from '@fuman/utils';
2
2
  import { CommitInfo, ConventionalCommit } from '../git/utils.js';
3
+ import { WorkspacePackage } from '../package-json/collect-package-jsons.js';
3
4
  import { ProjectChangedFile } from './collect-files.js';
4
5
  export interface ChangelogGeneratorParams {
5
6
  onCommitParseFailed?: (commit: CommitInfo) => void;
@@ -30,4 +31,11 @@ export interface VersioningOptions {
30
31
  */
31
32
  shouldInclude?: (file: ProjectChangedFile) => MaybePromise<boolean>;
32
33
  changelog?: ChangelogGeneratorParams;
34
+ /**
35
+ * hook that is called after the versions were bumped and pushed to registries,
36
+ * but before the release commit is created and pushed to git.
37
+ *
38
+ * can be used to add something to the release commit
39
+ */
40
+ beforeReleaseCommit?: (workspace: WorkspacePackage[]) => MaybePromise<void>;
33
41
  }