@fuman/build 0.0.1 → 0.0.3

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.
@@ -1,3 +1,6 @@
1
+ /** are we running in github actions? */
1
2
  export declare function isRunningInGithubActions(): boolean;
3
+ /** get the value of a github actions input */
2
4
  export declare function getGithubActionsInput(name: string): string | undefined;
5
+ /** set the value of a github actions output */
3
6
  export declare function writeGithubActionsOutput(name: string, value: string): void;
@@ -1,10 +1,22 @@
1
1
  import { WorkspacePackage } from '../../package-json/collect-package-jsons.js';
2
2
  import { bc } from './_utils.js';
3
+ /**
4
+ * build a single package using vite
5
+ *
6
+ * tiny wrapper on top of `vite build`
7
+ */
3
8
  export declare function buildPackage(params: {
9
+ /** path to the workspace root */
4
10
  workspaceRoot: string;
11
+ /**
12
+ * list of workspace packages **including root**
13
+ */
5
14
  workspace?: WorkspacePackage[];
15
+ /** name of the package to build */
6
16
  packageName: string;
17
+ /** path to the `build.config.js` file */
7
18
  configPath?: string;
19
+ /** "fixed" version to use when building the package */
8
20
  fixedVersion?: string;
9
21
  }): Promise<void>;
10
22
  export declare const buildPackageCli: bc.Command<{
@@ -16,8 +16,8 @@ function formatBumpVersionResult(result, withReleaseType) {
16
16
  lines.push(`next version: ${result.nextVersion}`);
17
17
  lines.push("");
18
18
  lines.push("list of changed packages:");
19
- for (const { package: pkg, because } of result.changedPackages) {
20
- lines.push(` ${pkg.json.name}${because ? ` (because of ${because.join(", ")})` : ""}: ${pkg.json.version} → ${result.nextVersion}`);
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}`);
21
21
  }
22
22
  return lines.join("\n");
23
23
  }
@@ -0,0 +1,11 @@
1
+ import { bc } from './_utils.js';
2
+ /** generate documentation using typedoc for the workspace */
3
+ export declare function generateDocs(params: {
4
+ /** path to the workspace root */
5
+ workspaceRoot: string;
6
+ }): Promise<void>;
7
+ export declare const generateDocsCli: bc.Command<{
8
+ root: string | undefined;
9
+ }, {
10
+ root: string | undefined;
11
+ }>;
@@ -0,0 +1,132 @@
1
+ import process from "node:process";
2
+ import { asNonNull } from "@fuman/utils";
3
+ import * as td from "typedoc";
4
+ import { loadBuildConfig } from "../../misc/_config.js";
5
+ import { collectPackageJsons } from "../../package-json/collect-package-jsons.js";
6
+ import { processPackageJson } from "../../package-json/process-package-json.js";
7
+ import * as bc from "@drizzle-team/brocli";
8
+ const CUSTOM_ROOT_FIELDS = [
9
+ "includePackages",
10
+ "excludePackages"
11
+ ];
12
+ const DEFAULT_CONFIG = {
13
+ includeVersion: true,
14
+ validation: {
15
+ notExported: true,
16
+ invalidLink: true,
17
+ notDocumented: false
18
+ },
19
+ excludePrivate: true,
20
+ excludeExternals: true,
21
+ excludeInternal: true,
22
+ exclude: [
23
+ "**/*/node_modules",
24
+ "**/*.test.ts",
25
+ "**/*.test-utils.ts"
26
+ ]
27
+ };
28
+ class FumanTypedocReader {
29
+ constructor(workspaceRoot) {
30
+ this.workspaceRoot = workspaceRoot;
31
+ }
32
+ name = "@fuman/build";
33
+ order = 0;
34
+ // before any other readers
35
+ supportsPackages = true;
36
+ _workspace;
37
+ _rootConfig;
38
+ _forwardOptions(options, config, cwd) {
39
+ for (const [key, val] of Object.entries(config)) {
40
+ if (CUSTOM_ROOT_FIELDS.includes(key)) continue;
41
+ options.setValue(key, val, cwd);
42
+ }
43
+ }
44
+ async read(options, logger, cwd) {
45
+ if (cwd === this.workspaceRoot) {
46
+ const config = await loadBuildConfig(cwd);
47
+ this._rootConfig = config;
48
+ const data2 = config?.typedoc;
49
+ if (data2 != null) this._forwardOptions(options, data2, cwd);
50
+ options.setValue("entryPointStrategy", "packages");
51
+ this._workspace = await collectPackageJsons(cwd);
52
+ const entrypoints2 = [];
53
+ for (const pkg2 of this._workspace) {
54
+ const pkgName = asNonNull(pkg2.json.name);
55
+ if (data2?.includePackages && !data2.includePackages.includes(pkgName)) continue;
56
+ if (data2?.excludePackages?.includes(pkgName)) continue;
57
+ if (pkg2.json.exports != null && !data2?.includePackages?.includes(pkgName)) continue;
58
+ entrypoints2.push(pkg2.path);
59
+ }
60
+ options.setValue("entryPoints", entrypoints2, cwd);
61
+ return;
62
+ }
63
+ const rootConfig = asNonNull(this._rootConfig);
64
+ if (rootConfig.typedoc != null) this._forwardOptions(options, rootConfig.typedoc, cwd);
65
+ const pkg = asNonNull(this._workspace?.find((pkg2) => pkg2.path.replace(/\/$/, "") === cwd.replace(/\/$/, "")));
66
+ const pkgConfig = await loadBuildConfig(cwd);
67
+ const hookContext = {
68
+ outDir: "",
69
+ packageDir: pkg.path,
70
+ packageName: asNonNull(pkg.json.name),
71
+ packageJson: pkg.json,
72
+ jsr: false,
73
+ typedoc: true
74
+ };
75
+ pkgConfig?.preparePackageJson?.(hookContext);
76
+ const { entrypoints } = processPackageJson({ packageJson: pkg.json, onlyEntrypoints: true });
77
+ options.setValue("entryPoints", Object.values(entrypoints), cwd);
78
+ if (!pkgConfig?.typedoc) return;
79
+ let data = pkgConfig.typedoc;
80
+ if (typeof data === "function") {
81
+ data = data(options.getRawValues());
82
+ }
83
+ this._forwardOptions(options, data, cwd);
84
+ }
85
+ }
86
+ async function generateDocs(params) {
87
+ const app = await td.Application.bootstrapWithPlugins(DEFAULT_CONFIG, [
88
+ new FumanTypedocReader(params.workspaceRoot),
89
+ new td.TSConfigReader(),
90
+ new td.TypeDocReader()
91
+ ]);
92
+ const project = await app.convert();
93
+ if (!project) {
94
+ throw new Error("Could not convert to typedoc project");
95
+ }
96
+ if (app.options.getValue("treatWarningsAsErrors") && app.logger.hasWarnings()) {
97
+ throw new Error("There were warnings while converting the project");
98
+ }
99
+ const preValidationWarnCount = app.logger.warningCount;
100
+ app.validate(project);
101
+ const hadValidationWarnings = app.logger.warningCount !== preValidationWarnCount;
102
+ if (app.logger.hasErrors()) {
103
+ throw new Error("There were errors while validating the project");
104
+ }
105
+ if (hadValidationWarnings && (app.options.getValue("treatWarningsAsErrors") || app.options.getValue("treatValidationWarningsAsErrors"))) {
106
+ throw new Error("There were warnings while validating the project");
107
+ }
108
+ if (app.options.getValue("emit") === "none") return;
109
+ await app.generateOutputs(project);
110
+ if (app.logger.hasErrors()) {
111
+ throw new Error("There were errors while generating the outputs");
112
+ }
113
+ if (app.options.getValue("treatWarningsAsErrors") && app.logger.hasWarnings()) {
114
+ throw new Error("There were warnings while generating the outputs");
115
+ }
116
+ }
117
+ const generateDocsCli = bc.command({
118
+ name: "typedoc",
119
+ desc: "generate docs using typedoc",
120
+ options: {
121
+ root: bc.string().desc("path to the root of the workspace (default: cwd)")
122
+ },
123
+ handler: async (args) => {
124
+ await generateDocs({
125
+ workspaceRoot: args.root ?? process.cwd()
126
+ });
127
+ }
128
+ });
129
+ export {
130
+ generateDocs,
131
+ generateDocsCli
132
+ };
@@ -1,7 +1,11 @@
1
1
  import { bc } from './_utils.js';
2
+ /** generate a graphviz dot file of the workspace dependencies */
2
3
  export declare function generateDepsGraph(params: {
4
+ /** path to the workspace root */
3
5
  workspaceRoot: string | URL;
6
+ /** whether to include the root package.json in the graph */
4
7
  includeRoot?: boolean;
8
+ /** whether to include external dependencies in the graph */
5
9
  includeExternal?: boolean;
6
10
  }): Promise<string>;
7
11
  export declare const generateDepsGraphCli: bc.Command<{
@@ -4,6 +4,7 @@ export declare const releaseCli: bc.Command<{
4
4
  withGithubRelease: boolean;
5
5
  githubToken: string | undefined;
6
6
  githubRepo: string | undefined;
7
+ githubApiUrl: string | undefined;
7
8
  withJsr: boolean;
8
9
  jsrRegistry: string | undefined;
9
10
  jsrToken: string | undefined;
@@ -20,6 +21,7 @@ export declare const releaseCli: bc.Command<{
20
21
  withGithubRelease: boolean;
21
22
  githubToken: string | undefined;
22
23
  githubRepo: string | undefined;
24
+ githubApiUrl: string | undefined;
23
25
  withJsr: boolean;
24
26
  jsrRegistry: string | undefined;
25
27
  jsrToken: string | undefined;
@@ -5,7 +5,7 @@ import { nodeReadableToWeb } from "@fuman/node";
5
5
  import { asNonNull, notImplemented } from "@fuman/utils";
6
6
  import { sort } from "semver";
7
7
  import { createGithubRelease } from "../../git/github.js";
8
- import { getLatestTag, getFirstCommit } from "../../git/utils.js";
8
+ import { getLatestTag, getFirstCommit, gitTagExists } from "../../git/utils.js";
9
9
  import { jsrCreatePackages } from "../../jsr/create-packages.js";
10
10
  import { generateDenoWorkspace } from "../../jsr/generate-workspace.js";
11
11
  import { exec } from "../../misc/exec.js";
@@ -24,6 +24,7 @@ const releaseCli = bc.command({
24
24
  withGithubRelease: bc.boolean("with-github-release").desc("whether to create a github release (requires GITHUB_TOKEN env var). if false, will only create a commit with the release notes").default(false),
25
25
  githubToken: bc.string("github-token").desc("github token to use for creating a release (defaults to GITHUB_TOKEN env var)"),
26
26
  githubRepo: bc.string("github-repo").desc("github repo to create a release for (defaults to GITHUB_REPOSITORY env var)"),
27
+ githubApiUrl: bc.string("github-api-url").desc("github api url to use for creating a release (for github-compatible apis)"),
27
28
  withJsr: bc.boolean("with-jsr").desc("whether to publish to jsr").default(false),
28
29
  jsrRegistry: bc.string("jsr-registry").desc("URL of the jsr registry to publish to"),
29
30
  jsrToken: bc.string("jsr-token").desc("jsr token to use for publishing"),
@@ -63,14 +64,32 @@ const releaseCli = bc.command({
63
64
  dryRun: args.dryRun
64
65
  });
65
66
  changedPackages = bumpVersionResult.changedPackages.map((pkg) => pkg.package);
66
- if (bumpVersionResult.changedPackages.length === 0) {
67
+ if (changedPackages.length === 0) {
67
68
  console.log("🤔 no packages changed, nothing to do");
68
69
  process.exit(1);
69
70
  }
70
71
  console.log(formatBumpVersionResult(bumpVersionResult, args.kind === "auto"));
72
+ for (const pkg of changedPackages) {
73
+ const inWorkspace = asNonNull(workspace.find((it) => it.json.name === pkg.json.name));
74
+ inWorkspace.json.version = pkg.json.version;
75
+ }
71
76
  } else {
72
77
  changedPackages = workspace;
73
78
  }
79
+ let tagName;
80
+ if (!bumpVersionResult) {
81
+ const versions = sort(workspace.map((pkg) => asNonNull(pkg.json.version)));
82
+ tagName = `v${versions[versions.length - 1]}`;
83
+ } else {
84
+ tagName = `v${bumpVersionResult.nextVersion}`;
85
+ }
86
+ if (await gitTagExists(tagName, root)) {
87
+ console.log(`❗ tag ${tagName} already exists. did the previous release complete successfully?`);
88
+ console.log("❗ if so, please verify versions in package.json and try again");
89
+ if (!args.dryRun) {
90
+ process.exit(1);
91
+ }
92
+ }
74
93
  console.log("");
75
94
  console.log("📝 generating changelog...");
76
95
  const changelog = prevTag != null ? await generateChangelog({
@@ -154,17 +173,10 @@ const releaseCli = bc.command({
154
173
  });
155
174
  console.log("\x1B[;32m✅ published to jsr\x1B[;0m");
156
175
  }
157
- let tagName;
158
- if (!bumpVersionResult) {
159
- const versions = sort(workspace.map((pkg) => asNonNull(pkg.json.version)));
160
- tagName = `v${versions[versions.length - 1]}`;
161
- } else {
162
- tagName = `v${bumpVersionResult.nextVersion}`;
163
- }
164
176
  if (args.dryRun) {
165
177
  console.log("dry run, skipping release commit and tag");
166
178
  } else {
167
- let message = tagName;
179
+ let message = `chore(release): ${tagName}`;
168
180
  if (!args.withGithubRelease) {
169
181
  message += `
170
182
 
@@ -1,4 +1,5 @@
1
1
  import { bc } from './_utils.js';
2
+ /** information about a mismatch between a package and its dependencies */
2
3
  export interface WorkspaceDepsError {
3
4
  /** package name where the mismatch occurred */
4
5
  package: string;
@@ -13,7 +14,12 @@ export interface WorkspaceDepsError {
13
14
  /** original version of the dependency */
14
15
  otherVersion: string;
15
16
  }
16
- export declare function validateWorkspaceDeps(workspaceRoot: string | URL, params?: {
17
+ /**
18
+ * validate the external dependencies of a workspace
19
+ */
20
+ export declare function validateWorkspaceDeps(params: {
21
+ /** path to the workspace root */
22
+ workspaceRoot: string | URL;
17
23
  /**
18
24
  * whether to also validate the root package.json
19
25
  *
@@ -2,8 +2,12 @@ import process from "node:process";
2
2
  import { satisfies } from "semver";
3
3
  import { collectPackageJsons } from "../../package-json/collect-package-jsons.js";
4
4
  import * as bc from "@drizzle-team/brocli";
5
- async function validateWorkspaceDeps(workspaceRoot, params) {
6
- const { includeRoot = true, skipWorkspaceDeps = true } = params || {};
5
+ async function validateWorkspaceDeps(params) {
6
+ const {
7
+ workspaceRoot,
8
+ includeRoot = true,
9
+ skipWorkspaceDeps = true
10
+ } = params;
7
11
  const pjs = await collectPackageJsons(workspaceRoot, includeRoot);
8
12
  const workspacePackages = new Set(skipWorkspaceDeps ? pjs.map((pj) => pj.json.name) : []);
9
13
  const versions = {};
@@ -48,7 +52,8 @@ const validateWorkspaceDepsCli = bc.command({
48
52
  root: bc.string().desc("path to the root of the workspace (default: cwd)")
49
53
  },
50
54
  handler: async (args) => {
51
- const errors = await validateWorkspaceDeps(args.root ?? process.cwd(), {
55
+ const errors = await validateWorkspaceDeps({
56
+ workspaceRoot: args.root ?? process.cwd(),
52
57
  includeRoot: args.includeRoot,
53
58
  skipWorkspaceDeps: !args.noSkipWorkspaceDeps
54
59
  });
package/cli/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { buildPackage } from './commands/build.js';
2
+ export { generateDocs } from './commands/docs.js';
2
3
  export { generateDepsGraph } from './commands/gen-deps-graph.js';
3
- export { validateWorkspaceDeps } from './commands/validate-workspace-deps.js';
4
+ export { validateWorkspaceDeps, type WorkspaceDepsError } from './commands/validate-workspace-deps.js';
package/config.d.ts CHANGED
@@ -1,6 +1,9 @@
1
+ import { AnyToNever } from '@fuman/utils';
2
+ import { TypeDocOptions } from 'typedoc';
1
3
  import { JsrConfig } from './jsr/config.js';
2
4
  import { PackageJson } from './package-json/types.js';
3
5
  import { VersioningOptions } from './versioning/types.js';
6
+ /** context object that is passed to each build hook */
4
7
  export interface BuildHookContext {
5
8
  /** full path to the output directory */
6
9
  outDir: string;
@@ -15,7 +18,10 @@ export interface BuildHookContext {
15
18
  packageJson: PackageJson;
16
19
  /** whether this is a jsr build */
17
20
  jsr: boolean;
21
+ /** whether this is a documentation build */
22
+ typedoc: boolean;
18
23
  }
24
+ /** root configuration object */
19
25
  export interface RootConfigObject {
20
26
  /**
21
27
  * path to vite config to use when building using fuman-build cli,
@@ -28,5 +34,21 @@ export interface RootConfigObject {
28
34
  jsr?: JsrConfig;
29
35
  /** configuration for the changelog generator */
30
36
  versioning?: VersioningOptions;
37
+ /** base configuration for typedoc */
38
+ typedoc?: AnyToNever<Omit<Partial<TypeDocOptions>, 'entryPoints' | 'entryPointStrategy' | 'extends'> & {
39
+ /**
40
+ * **note**: fuman-specific option
41
+ *
42
+ * if passed, docs will be generated for the specified packages only
43
+ */
44
+ includePackages?: string[];
45
+ /**
46
+ * **note**: fuman-specific option
47
+ *
48
+ * list of packages for which the docs should *not* be generated
49
+ */
50
+ excludePackages?: string[];
51
+ }>;
31
52
  }
53
+ /** root configuration (either an object or a function that returns an object) */
32
54
  export type RootConfig = RootConfigObject | (() => RootConfigObject);
package/fuman-build.js CHANGED
@@ -4,6 +4,7 @@ import * as bc from "@drizzle-team/brocli";
4
4
  import { buildPackageCli } from "./cli/commands/build.js";
5
5
  import { bumpVersionCli } from "./cli/commands/bump-version.js";
6
6
  import { runContinuousReleaseCli } from "./cli/commands/cr.js";
7
+ import { generateDocsCli } from "./cli/commands/docs.js";
7
8
  import { findChangedPackagesCli } from "./cli/commands/find-changed-packages.js";
8
9
  import { generateChangelogCli } from "./cli/commands/gen-changelog.js";
9
10
  import { generateDepsGraphCli } from "./cli/commands/gen-deps-graph.js";
@@ -21,7 +22,8 @@ await bc.run([
21
22
  bumpVersionCli,
22
23
  publishPackagesCli,
23
24
  releaseCli,
24
- runContinuousReleaseCli
25
+ runContinuousReleaseCli,
26
+ generateDocsCli
25
27
  ], {
26
28
  theme: (event) => {
27
29
  if (event.type === "error" && event.violation === "unknown_error") {
package/git/github.d.ts CHANGED
@@ -11,4 +11,5 @@ export declare function createGithubRelease(params: {
11
11
  type: string;
12
12
  body: BodyInit;
13
13
  }[];
14
+ apiUrl?: string;
14
15
  }): Promise<number>;
package/git/github.js CHANGED
@@ -4,7 +4,7 @@ import { asyncPool } from "@fuman/utils";
4
4
  import { z } from "zod";
5
5
  async function createGithubRelease(params) {
6
6
  const ffetch = ffetchBase.extend({
7
- baseUrl: "https://api.github.com",
7
+ baseUrl: params.apiUrl ?? "https://api.github.com",
8
8
  addons: [
9
9
  ffetchAddons.retry(),
10
10
  ffetchAddons.parser(ffetchZodAdapter())
package/git/utils.d.ts CHANGED
@@ -1,38 +1,94 @@
1
+ /**
2
+ * get the latest tag in the repository
3
+ * @param cwd override the current working directory
4
+ */
1
5
  export declare function getLatestTag(cwd?: string | URL): Promise<string | null>;
6
+ /**
7
+ * get hash of the first commit in the repository
8
+ * @param cwd override the current working directory
9
+ */
2
10
  export declare function getFirstCommit(cwd?: string | URL): Promise<string>;
11
+ /**
12
+ * get hash of the current commit
13
+ * @param cwd override the current working directory
14
+ */
3
15
  export declare function getCurrentCommit(cwd?: string | URL): Promise<string>;
16
+ /**
17
+ * get name of the current branch
18
+ * @param cwd override the current working directory
19
+ */
4
20
  export declare function getCurrentBranch(cwd?: string | URL): Promise<string>;
21
+ export declare function gitTagExists(tag: string, cwd?: string | URL): Promise<boolean>;
22
+ /**
23
+ * find changed files between two commits
24
+ *
25
+ * @returns list of changed files, relative to the repository root
26
+ */
5
27
  export declare function findChangedFiles(params: {
28
+ /** starting point for the diff */
6
29
  since: string;
7
- /** @default 'HEAD' */
30
+ /**
31
+ * ending point for the diff
32
+ *
33
+ * @default 'HEAD'
34
+ */
8
35
  until?: string;
36
+ /** override the current working directory */
9
37
  cwd?: string | URL;
10
38
  }): Promise<string[]>;
39
+ /** information about a commit */
11
40
  export interface CommitInfo {
41
+ /** full hash of the commit */
12
42
  hash: string;
43
+ /** author of the commit */
13
44
  author: {
45
+ /** name of the author */
14
46
  name: string;
47
+ /** email of the author */
15
48
  email: string;
49
+ /** date of the commit */
16
50
  date: Date;
17
51
  };
52
+ /** committer of the commit */
18
53
  committer: {
54
+ /** name of the committer */
19
55
  name: string;
56
+ /** email of the committer */
20
57
  email: string;
58
+ /** date of the commit */
21
59
  date: Date;
22
60
  };
61
+ /** commit message */
23
62
  message: string;
63
+ /** commit description */
24
64
  description: string;
25
65
  }
66
+ /**
67
+ * get information about commits between two commits (both ends inclusive)
68
+ *
69
+ * @returns list of commits, in reverse chronological order
70
+ */
26
71
  export declare function getCommitsBetween(params: {
72
+ /** starting point for the diff */
27
73
  since: string;
28
- /** @default 'HEAD' */
74
+ /**
75
+ * ending point for the diff
76
+ * @default 'HEAD'
77
+ */
29
78
  until?: string;
79
+ /** override the current working directory */
30
80
  cwd?: string | URL;
31
81
  }): Promise<CommitInfo[]>;
82
+ /** information about a conventional commit */
32
83
  export interface ConventionalCommit {
84
+ /** type of the commit (e.g. `feat`, `fix`, etc) */
33
85
  type: string;
86
+ /** scope of the commit (i.e. "scope" in `feat(scope): subject`) */
34
87
  scope?: string;
88
+ /** whether the commit is marked as breaking with an exclamation mark */
35
89
  breaking: boolean;
90
+ /** subject of the commit */
36
91
  subject: string;
37
92
  }
93
+ /** parse a conventional commit message */
38
94
  export declare function parseConventionalCommit(msg: string): ConventionalCommit | null;
package/git/utils.js CHANGED
@@ -30,6 +30,13 @@ async function getCurrentBranch(cwd) {
30
30
  });
31
31
  return res.stdout.trim();
32
32
  }
33
+ async function gitTagExists(tag, cwd) {
34
+ const res = await exec(["git", "tag", "--list", tag], {
35
+ cwd,
36
+ throwOnError: true
37
+ });
38
+ return res.stdout.trim() !== "";
39
+ }
33
40
  async function findChangedFiles(params) {
34
41
  const { since, until = "HEAD", cwd } = params;
35
42
  const res = await exec(["git", "diff", "--name-only", since, until], {
@@ -106,5 +113,6 @@ export {
106
113
  getCurrentCommit,
107
114
  getFirstCommit,
108
115
  getLatestTag,
116
+ gitTagExists,
109
117
  parseConventionalCommit
110
118
  };
package/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export * from './git/index.js';
5
5
  export * from './misc/index.js';
6
6
  export * from './npm/index.js';
7
7
  export * from './package-json/index.js';
8
+ export * from './versioning/index.js';
package/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { getGithubActionsInput, isRunningInGithubActions, writeGithubActionsOutput } from "./ci/github-actions.js";
2
2
  import { buildPackage } from "./cli/commands/build.js";
3
+ import { generateDocs } from "./cli/commands/docs.js";
3
4
  import { generateDepsGraph } from "./cli/commands/gen-deps-graph.js";
4
5
  import { validateWorkspaceDeps } from "./cli/commands/validate-workspace-deps.js";
5
- import { findChangedFiles, getCommitsBetween, getCurrentBranch, getCurrentCommit, getFirstCommit, getLatestTag, parseConventionalCommit } from "./git/utils.js";
6
+ import { findChangedFiles, getCommitsBetween, getCurrentBranch, getCurrentCommit, getFirstCommit, getLatestTag, gitTagExists, parseConventionalCommit } from "./git/utils.js";
6
7
  import { exec } from "./misc/exec.js";
7
8
  import { normalizeFilePath } from "./misc/path.js";
8
9
  import { determinePublishOrder, sortWorkspaceByPublishOrder } from "./misc/publish-order.js";
@@ -13,17 +14,25 @@ import { findPackageJson } from "./package-json/find-package-json.js";
13
14
  import { parsePackageJson, parsePackageJsonFile, parseWorkspaceRootPackageJson } from "./package-json/parse.js";
14
15
  import { processPackageJson } from "./package-json/process-package-json.js";
15
16
  import { PackageJsonSchema } from "./package-json/types.js";
17
+ import { bumpVersion } from "./versioning/bump-version.js";
18
+ import { findProjectChangedFiles, findProjectChangedPackages } from "./versioning/collect-files.js";
19
+ import { generateChangelog } from "./versioning/generate-changelog.js";
16
20
  export {
17
21
  NPM_PACKAGE_NAME_REGEX,
18
22
  PackageJsonSchema,
19
23
  buildPackage,
24
+ bumpVersion,
20
25
  collectPackageJsons,
21
26
  determinePublishOrder,
22
27
  exec,
23
28
  filterPackageJsonsForPublish,
24
29
  findChangedFiles,
25
30
  findPackageJson,
31
+ findProjectChangedFiles,
32
+ findProjectChangedPackages,
33
+ generateChangelog,
26
34
  generateDepsGraph,
35
+ generateDocs,
27
36
  getCommitsBetween,
28
37
  getCurrentBranch,
29
38
  getCurrentCommit,
@@ -32,6 +41,7 @@ export {
32
41
  getLatestTag,
33
42
  getTsconfigFiles,
34
43
  getTsconfigFor,
44
+ gitTagExists,
35
45
  isRunningInGithubActions,
36
46
  normalizeFilePath,
37
47
  npmCheckVersion,
package/jsr/build-jsr.js CHANGED
@@ -103,7 +103,8 @@ ${badImports.join("\n")}`);
103
103
  packageDir: ourPackage.path,
104
104
  packageName: asNonNull(ourPackage.json.name),
105
105
  packageJson: ourPackage.json,
106
- jsr: true
106
+ jsr: true,
107
+ typedoc: false
107
108
  };
108
109
  packageConfig?.preparePackageJson?.(hookContext);
109
110
  const workspaceVersions = collectVersions(workspacePackages);
@@ -125,7 +125,8 @@ async function generateDenoWorkspace(params) {
125
125
  packageDir: packageOutRoot,
126
126
  packageName: pkg.json.name,
127
127
  packageJson: pkg.json,
128
- jsr: true
128
+ jsr: true,
129
+ typedoc: false
129
130
  };
130
131
  packageConfig?.preparePackageJson?.(hookContext);
131
132
  const workspaceVersions = collectVersions(workspacePackages);
package/misc/exec.d.ts CHANGED
@@ -1,9 +1,23 @@
1
1
  import { SpawnOptions } from 'node:child_process';
2
+ /** result of {@link exec} */
2
3
  export interface ExecResult {
4
+ /** stdout of the command */
3
5
  stdout: string;
6
+ /** stderr of the command */
4
7
  stderr: string;
8
+ /** exit code of the command */
5
9
  exitCode: number;
6
10
  }
11
+ /**
12
+ * execute a command and return its result
13
+ *
14
+ * **differences from node's `child_process.exec()`**:
15
+ * - 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`
17
+ *
18
+ * @param cmd command to execute (first element is the command itself, the rest are arguments to it)
19
+ */
7
20
  export declare function exec(cmd: string[], options?: SpawnOptions & {
8
21
  throwOnError?: boolean;
22
+ quiet?: boolean;
9
23
  }): Promise<ExecResult>;
package/misc/exec.js CHANGED
@@ -1,8 +1,18 @@
1
- import { spawn } from "node:child_process";
1
+ import path__default from "node:path";
2
+ import process from "node:process";
3
+ import { spawn } from "cross-spawn";
4
+ import { normalizeFilePath } from "./path.js";
2
5
  function exec(cmd, options) {
3
6
  return new Promise((resolve, reject) => {
4
- if (options?.stdio === "inherit") {
5
- console.log("\x1B[;34m$\x1B[;0m", cmd.map((it) => it.includes(" ") ? `"${it.replace(/"/g, '\\"')}"` : it).join(" "));
7
+ if (options?.stdio === "inherit" && !options.quiet) {
8
+ const cmdStr = cmd.map((it) => it.includes(" ") ? `"${it.replace(/"/g, '\\"')}"` : it).join(" ");
9
+ let cwdStr = "";
10
+ if (options?.cwd != null) {
11
+ const normCwd = path__default.resolve(normalizeFilePath(options.cwd));
12
+ const showCwd = normCwd !== process.cwd();
13
+ cwdStr = showCwd ? `\x1B[;3m${path__default.relative(process.cwd(), normCwd)}\x1B[;23m ` : "";
14
+ }
15
+ console.log(`${cwdStr}\x1B[;34m$\x1B[;0m ${cmdStr}`);
6
16
  }
7
17
  const proc = spawn(cmd[0], cmd.slice(1), {
8
18
  stdio: "pipe",
package/misc/path.d.ts CHANGED
@@ -1 +1,2 @@
1
- export declare function normalizeFilePath(filePath: string | URL): string;
1
+ import { URL as NodeURL } from 'node:url';
2
+ export declare function normalizeFilePath(filePath: string | URL | NodeURL): string;
package/misc/path.js CHANGED
@@ -1,6 +1,6 @@
1
- import { fileURLToPath } from "node:url";
1
+ import { URL as URL$1, fileURLToPath } from "node:url";
2
2
  function normalizeFilePath(filePath) {
3
- if (filePath instanceof URL) {
3
+ if (filePath instanceof URL || filePath instanceof URL$1) {
4
4
  return fileURLToPath(filePath);
5
5
  } else if (filePath.startsWith("file://")) {
6
6
  return fileURLToPath(new URL(filePath));
@@ -1,3 +1,12 @@
1
1
  import { WorkspacePackage } from '../package-json/collect-package-jsons.js';
2
+ /** sort a workspace by the publish order (i.e. dependencies first, then dependants) */
2
3
  export declare function sortWorkspaceByPublishOrder(packages: WorkspacePackage[]): WorkspacePackage[];
4
+ /**
5
+ * determine the publish order of a workspace
6
+ *
7
+ * publish order is determined by the dependencies of the packages,
8
+ * so that for each package, all its dependencies are to be published before it
9
+ *
10
+ * @throws if there's a cycle in the dependencies
11
+ */
3
12
  export declare function determinePublishOrder(dependencies: Record<string, string[]>): string[];
@@ -1,2 +1,6 @@
1
+ /** get a full tsconfig for the given directory */
1
2
  export declare function getTsconfigFor(cwd: string): Promise<unknown>;
3
+ /**
4
+ * get the list of files that are included in the tsconfig
5
+ */
2
6
  export declare function getTsconfigFiles(cwd: string): Promise<string[]>;
@@ -1,8 +1,17 @@
1
1
  import { PackageJson } from './types.js';
2
+ /** information about a package in a workspace */
2
3
  export interface WorkspacePackage {
4
+ /** path to the package root */
3
5
  path: string;
6
+ /** whether this is the root package */
4
7
  root: boolean;
8
+ /** package.json of the package */
5
9
  json: PackageJson;
6
10
  }
11
+ /** collect package.jsons from a workspace */
7
12
  export declare function collectPackageJsons(workspaceRoot: string | URL, includeRoot?: boolean): Promise<WorkspacePackage[]>;
13
+ /**
14
+ * filter packages to only include the ones that are to be published
15
+ * to the given registry
16
+ */
8
17
  export declare function filterPackageJsonsForPublish(packages: WorkspacePackage[], registry: 'jsr' | 'npm'): WorkspacePackage[];
@@ -1,4 +1,7 @@
1
1
  import { PackageJson } from './types.js';
2
+ /** parse a package.json from string */
2
3
  export declare function parsePackageJson(packageJson: string): PackageJson;
4
+ /** parse a package.json file */
3
5
  export declare function parsePackageJsonFile(packageJsonPath: string | URL): Promise<PackageJson>;
6
+ /** parse the package.json file at the root of the workspace */
4
7
  export declare function parseWorkspaceRootPackageJson(workspaceRoot: string | URL): Promise<PackageJson>;
@@ -26,10 +26,19 @@ async function parseWorkspaceRootPackageJson(workspaceRoot) {
26
26
  if (!workspace.packages) {
27
27
  throw new Error("No packages found in pnpm-workspace.yaml");
28
28
  }
29
- return {
30
- ...parsed,
31
- workspaces: workspace.packages
32
- };
29
+ if (workspace.catalog || workspace.catalogs) {
30
+ const catalogs = {};
31
+ if (workspace.catalog) {
32
+ catalogs[""] = workspace.catalog;
33
+ }
34
+ if (workspace.catalogs) {
35
+ for (const [name, catalog] of Object.entries(workspace.catalogs)) {
36
+ catalogs[name] = catalog;
37
+ }
38
+ }
39
+ parsed.catalogs = catalogs;
40
+ }
41
+ parsed.workspaces = workspace.packages;
33
42
  }
34
43
  return parsed;
35
44
  }
@@ -1,6 +1,7 @@
1
1
  import { PackageJson } from './types.js';
2
2
  export declare function processPackageJson(params: {
3
3
  packageJson: PackageJson;
4
+ onlyEntrypoints?: boolean;
4
5
  workspaceVersions?: Record<string, string>;
5
6
  bundledWorkspaceDeps?: RegExp[];
6
7
  rootPackageJson?: PackageJson;
@@ -2,6 +2,7 @@ const DEFAULT_FIELDS_TO_COPY_ROOT = ["license", "author", "contributors", "homep
2
2
  function processPackageJson(params) {
3
3
  const {
4
4
  packageJson: packageJsonOrig,
5
+ onlyEntrypoints = false,
5
6
  workspaceVersions,
6
7
  rootPackageJson,
7
8
  rootFieldsToCopy = DEFAULT_FIELDS_TO_COPY_ROOT,
@@ -10,71 +11,81 @@ function processPackageJson(params) {
10
11
  } = params;
11
12
  const packageJson = structuredClone(packageJsonOrig);
12
13
  const entrypoints = {};
13
- for (const field of rootFieldsToCopy) {
14
- if (rootPackageJson?.[field] != null && packageJson[field] == null) {
15
- packageJson[field] = rootPackageJson[field];
16
- }
17
- }
18
- const newScripts = {};
19
- if (packageJson.scripts && Array.isArray(packageJson.fuman?.keepScripts)) {
20
- for (const script of packageJson.fuman.keepScripts) {
21
- if (typeof script !== "string") continue;
22
- if (script in packageJson.scripts) continue;
23
- newScripts[script] = packageJson.scripts[script];
24
- }
25
- delete packageJson.keepScripts;
26
- }
27
- packageJson.scripts = newScripts;
28
- delete packageJson.devDependencies;
29
- delete packageJson.private;
30
- if (packageJson.fuman?.distOnlyFields) {
31
- Object.assign(packageJson, packageJson.fuman.distOnlyFields);
32
- delete packageJson.distOnlyFields;
33
- }
34
- function replaceWorkspaceDependencies(field) {
35
- if (packageJson[field] == null) return;
36
- const dependencies = packageJson[field];
37
- for (const name of Object.keys(dependencies)) {
38
- const value = dependencies[name];
39
- if (value.startsWith("workspace:")) {
40
- if (bundledWorkspaceDeps) {
41
- let found = false;
42
- for (const dep of bundledWorkspaceDeps) {
43
- if (dep.test(name)) {
44
- delete dependencies[name];
45
- found = true;
46
- break;
14
+ if (!onlyEntrypoints) {
15
+ let replaceCustomDependencies = function(field) {
16
+ if (packageJson[field] == null) return;
17
+ const dependencies = packageJson[field];
18
+ for (const name of Object.keys(dependencies)) {
19
+ const value = dependencies[name];
20
+ if (value.startsWith("workspace:")) {
21
+ if (bundledWorkspaceDeps) {
22
+ let found = false;
23
+ for (const dep of bundledWorkspaceDeps) {
24
+ if (dep.test(name)) {
25
+ delete dependencies[name];
26
+ found = true;
27
+ break;
28
+ }
47
29
  }
30
+ if (found) continue;
48
31
  }
49
- if (found) continue;
50
- }
51
- if (value !== "workspace:^" && value !== "workspace:*") {
52
- throw new Error(
53
- `Cannot replace workspace dependency ${name} with ${value} - only workspace:^ and * are supported`
54
- );
55
- }
56
- if (workspaceVersions?.[name] == null) {
57
- throw new Error(`Cannot replace workspace: dependency ${name} not found in workspace`);
32
+ if (value !== "workspace:^" && value !== "workspace:*") {
33
+ throw new Error(
34
+ `Cannot replace workspace dependency ${name} with ${value} - only workspace:^ and * are supported`
35
+ );
36
+ }
37
+ if (workspaceVersions?.[name] == null) {
38
+ throw new Error(`Cannot replace workspace: dependency ${name} not found in workspace`);
39
+ }
40
+ if (fixedVersion != null) {
41
+ dependencies[name] = fixedVersion;
42
+ continue;
43
+ }
44
+ const workspaceVersion = workspaceVersions?.[name];
45
+ const depVersion = value === "workspace:*" ? workspaceVersion : `^${workspaceVersion}`;
46
+ dependencies[name] = depVersion;
58
47
  }
59
- if (fixedVersion != null) {
60
- dependencies[name] = fixedVersion;
61
- continue;
48
+ if (value.startsWith("catalog:")) {
49
+ if (!rootPackageJson?.catalogs) throw new Error("catalogs are not available in the workspace root");
50
+ const catalogName = value.slice("catalog:".length);
51
+ const catalog = rootPackageJson.catalogs[catalogName];
52
+ if (catalog == null) throw new Error(`catalog ${catalogName} not found in the workspace root`);
53
+ if (catalog[name] == null) throw new Error(`catalog ${catalogName} does not contain ${name}`);
54
+ dependencies[name] = catalog[name];
62
55
  }
63
- const workspaceVersion = workspaceVersions?.[name];
64
- const depVersion = value === "workspace:*" ? workspaceVersion : `^${workspaceVersion}`;
65
- dependencies[name] = depVersion;
66
56
  }
57
+ };
58
+ for (const field of rootFieldsToCopy) {
59
+ if (rootPackageJson?.[field] != null && packageJson[field] == null) {
60
+ packageJson[field] = rootPackageJson[field];
61
+ }
62
+ }
63
+ const newScripts = {};
64
+ if (packageJson.scripts && Array.isArray(packageJson.fuman?.keepScripts)) {
65
+ for (const script of packageJson.fuman.keepScripts) {
66
+ if (typeof script !== "string") continue;
67
+ if (script in packageJson.scripts) continue;
68
+ newScripts[script] = packageJson.scripts[script];
69
+ }
70
+ delete packageJson.keepScripts;
71
+ }
72
+ packageJson.scripts = newScripts;
73
+ delete packageJson.devDependencies;
74
+ delete packageJson.private;
75
+ if (packageJson.fuman?.distOnlyFields) {
76
+ Object.assign(packageJson, packageJson.fuman.distOnlyFields);
77
+ delete packageJson.distOnlyFields;
67
78
  }
79
+ replaceCustomDependencies("dependencies");
80
+ replaceCustomDependencies("devDependencies");
81
+ replaceCustomDependencies("peerDependencies");
82
+ replaceCustomDependencies("optionalDependencies");
83
+ delete packageJson.typedoc;
84
+ delete packageJson.eslintConfig;
85
+ delete packageJson.eslintIgnore;
86
+ delete packageJson.prettier;
87
+ delete packageJson.fuman;
68
88
  }
69
- replaceWorkspaceDependencies("dependencies");
70
- replaceWorkspaceDependencies("devDependencies");
71
- replaceWorkspaceDependencies("peerDependencies");
72
- replaceWorkspaceDependencies("optionalDependencies");
73
- delete packageJson.typedoc;
74
- delete packageJson.eslintConfig;
75
- delete packageJson.eslintIgnore;
76
- delete packageJson.prettier;
77
- delete packageJson.fuman;
78
89
  if (packageJson.exports != null) {
79
90
  let exports = packageJson.exports;
80
91
  if (typeof exports === "string") {
@@ -13,6 +13,7 @@ export interface PackageJson {
13
13
  url: string;
14
14
  });
15
15
  keywords?: string[];
16
+ catalogs?: Record<string, Record<string, string>>;
16
17
  workspaces?: string[];
17
18
  scripts?: Record<string, string>;
18
19
  dependencies?: Record<string, string>;
package/package.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
2
  "name": "@fuman/build",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
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.1",
11
- "@fuman/io": "^0.0.1",
12
- "@fuman/node": "^0.0.1",
13
- "@fuman/utils": "^0.0.1",
10
+ "@fuman/fetch": "^0.0.3",
11
+ "@fuman/io": "^0.0.3",
12
+ "@fuman/node": "^0.0.3",
13
+ "@fuman/utils": "^0.0.3",
14
14
  "cross-spawn": "^7.0.5",
15
15
  "detect-indent": "^7.0.1",
16
16
  "js-yaml": "^4.1.0",
17
17
  "picomatch": "^4.0.2",
18
18
  "semver": "^7.6.3",
19
19
  "tinyglobby": "^0.2.6",
20
- "zod": "^3.23.8"
20
+ "zod": "^3.0.0"
21
21
  },
22
22
  "peerDependencies": {
23
+ "typedoc": ">=0.24.0",
23
24
  "typescript": "^5.2.2",
24
- "vite": "^5.4.0"
25
+ "vite": "^5.4.0 || ^6.0.0"
25
26
  },
26
27
  "exports": {
27
28
  ".": {
@@ -59,6 +60,11 @@
59
60
  "bin": {
60
61
  "fuman-build": "fuman-build.js"
61
62
  },
63
+ "peerDependenciesMeta": {
64
+ "typedoc": {
65
+ "optional": true
66
+ }
67
+ },
62
68
  "author": "",
63
69
  "repository": {
64
70
  "type": "git",
@@ -4,6 +4,8 @@ import { VersioningOptions } from './types.js';
4
4
  export interface BumpVersionPackage {
5
5
  /** info about the package */
6
6
  package: WorkspacePackage;
7
+ /** version before the bump */
8
+ prevVersion: string;
7
9
  /**
8
10
  * if the package was not changed by itself,
9
11
  * but the bump is required because of another package
@@ -72,7 +72,10 @@ async function bumpVersion(params) {
72
72
  const result = [];
73
73
  for (const pkg of changedPackages) {
74
74
  if (pkg.json.fuman?.ownVersioning) continue;
75
- result.push({ package: pkg });
75
+ result.push({
76
+ package: pkg,
77
+ prevVersion: asNonNull(pkg.json.version)
78
+ });
76
79
  }
77
80
  for (const pkg of changedPackages) {
78
81
  if (pkg.json.fuman?.ownVersioning) continue;
@@ -96,6 +99,7 @@ async function bumpVersion(params) {
96
99
  } else {
97
100
  result.push({
98
101
  package: otherPkg,
102
+ prevVersion: asNonNull(otherPkg.json.version),
99
103
  because: [pkgName]
100
104
  });
101
105
  }
@@ -104,8 +108,8 @@ async function bumpVersion(params) {
104
108
  }
105
109
  }
106
110
  }
107
- if (!dryRun) {
108
- for (const { package: pkg } of result) {
111
+ for (const { package: pkg } of result) {
112
+ if (!dryRun) {
109
113
  const pkgJsonPath = join(pkg.path, "package.json");
110
114
  const pkgJsonText = await fsp.readFile(pkgJsonPath, "utf8");
111
115
  const indent = detectIndent(pkgJsonText).indent || " ";
@@ -114,6 +118,7 @@ async function bumpVersion(params) {
114
118
  await fsp.writeFile(pkgJsonPath, `${JSON.stringify(pkgJson, null, indent)}
115
119
  `);
116
120
  }
121
+ pkg.json.version = nextVersion;
117
122
  }
118
123
  return {
119
124
  maxVersion,
@@ -0,0 +1,4 @@
1
+ export * from './bump-version.js';
2
+ export * from './collect-files.js';
3
+ export * from './generate-changelog.js';
4
+ export * from './types.js';
@@ -9,6 +9,7 @@ export interface ChangelogGeneratorParams {
9
9
  commitFormatter?: (commit: CommitInfo, parsed: ConventionalCommit, files: string[]) => string;
10
10
  packageCommitsFormatter?: (packageName: string, commits: Record<string, string>) => string;
11
11
  }
12
+ /** settings for versioning manager */
12
13
  export interface VersioningOptions {
13
14
  /**
14
15
  * globs of files changes to which to white-list (relative to package root)
@@ -52,7 +52,8 @@ async function fumanBuild(params) {
52
52
  packageDir: packageRoot,
53
53
  packageName: asNonNull(ourPackageJson.name),
54
54
  packageJson: ourPackageJson,
55
- jsr: false
55
+ jsr: false,
56
+ typedoc: false
56
57
  };
57
58
  params.preparePackageJson?.(hookContext);
58
59
  packageConfig?.preparePackageJson?.(hookContext);
package/vite/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { MaybePromise } from '@fuman/utils';
1
+ import { AnyToNever, MaybePromise } from '@fuman/utils';
2
+ import { TypeDocOptions } from 'typedoc';
2
3
  import { Plugin, UserConfig } from 'vite';
3
4
  import { BuildHookContext } from '../config.js';
4
5
  import { JsrConfig } from '../jsr/config.js';
@@ -30,5 +31,7 @@ export interface CustomBuildConfigObject {
30
31
  * time to do any final modifications to the package contents
31
32
  */
32
33
  finalize?: (ctx: BuildHookContext) => MaybePromise<void>;
34
+ /** package-specific configuration for typedoc */
35
+ typedoc?: AnyToNever<Partial<TypeDocOptions>> | ((current: AnyToNever<Partial<TypeDocOptions>>) => AnyToNever<Partial<TypeDocOptions>>);
33
36
  }
34
37
  export type CustomBuildConfig = CustomBuildConfigObject | (() => CustomBuildConfigObject);