bun-workspaces 0.2.0 → 1.0.0-alpha

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,17 +1,145 @@
1
- import createLogger from "pino";
1
+ import { IS_PRODUCTION, IS_TEST } from "./env";
2
2
 
3
- export const logger = createLogger({
4
- msgPrefix: "[bun-workspaces] ",
5
- level:
6
- process.env.NODE_ENV === "test"
3
+ export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
4
+
5
+ const getLevelNumber = (level: LogLevel) => LOG_LEVELS.indexOf(level);
6
+
7
+ export type LogLevel = (typeof LOG_LEVELS)[number];
8
+
9
+ export type LogLevelSetting = LogLevel | "silent";
10
+
11
+ export const validateLogLevel = (level: LogLevelSetting) => {
12
+ if (level === "silent") return;
13
+ if (!LOG_LEVELS.includes(level)) {
14
+ throw new Error(
15
+ `Invalid log level: "${level}". Accepted values: ${LOG_LEVELS.join(", ")}`,
16
+ );
17
+ }
18
+ };
19
+
20
+ export type LogMetadata = Record<string, any>;
21
+
22
+ export interface Log<
23
+ Message extends string | Error = string,
24
+ Metadata extends LogMetadata = LogMetadata,
25
+ > {
26
+ message: Message;
27
+ level: LogLevel;
28
+ metadata: Metadata;
29
+ time: Date;
30
+ }
31
+
32
+ export type Logger = {
33
+ name: string;
34
+
35
+ log<
36
+ Message extends string | Error = string,
37
+ Metadata extends LogMetadata = LogMetadata,
38
+ >(
39
+ message: Message,
40
+ level: LogLevel,
41
+ metadata?: Metadata,
42
+ ): Log<Message, Metadata>;
43
+
44
+ printLevel: LogLevelSetting;
45
+ } & {
46
+ [Level in LogLevel]: <
47
+ Message extends string | Error = string,
48
+ Metadata extends LogMetadata = LogMetadata,
49
+ >(
50
+ message: Message,
51
+ metadata?: Metadata,
52
+ ) => Log<Message, Metadata>;
53
+ };
54
+
55
+ export const createLogger = (name: string): Logger => new _Logger(name);
56
+
57
+ class _Logger implements Logger {
58
+ constructor(public name: string) {}
59
+
60
+ log<
61
+ Message extends string | Error = string,
62
+ Metadata extends LogMetadata = LogMetadata,
63
+ >(
64
+ message: Message,
65
+ level: LogLevel,
66
+ metadata?: Metadata,
67
+ ): Log<Message, Metadata> {
68
+ const log: Log<Message, Metadata> = {
69
+ message,
70
+ level,
71
+ metadata: metadata ?? ({} as Metadata),
72
+ time: new Date(),
73
+ };
74
+
75
+ if (this.shouldPrint(level)) {
76
+ const formattedMessage = this.formatLogMessage(message, level);
77
+ if (message instanceof Error) {
78
+ message.message = formattedMessage;
79
+ }
80
+ console[level](
81
+ message instanceof Error ? message : formattedMessage,
82
+ ...(metadata ? [{ metadata }] : []),
83
+ );
84
+ }
85
+
86
+ return log;
87
+ }
88
+
89
+ debug<
90
+ Message extends string | Error = string,
91
+ Metadata extends LogMetadata = LogMetadata,
92
+ >(message: Message, metadata?: Metadata): Log<Message, Metadata> {
93
+ return this.log(message, "debug", metadata);
94
+ }
95
+
96
+ info<
97
+ Message extends string | Error = string,
98
+ Metadata extends LogMetadata = LogMetadata,
99
+ >(message: Message, metadata?: Metadata): Log<Message, Metadata> {
100
+ return this.log(message, "info", metadata);
101
+ }
102
+
103
+ warn<
104
+ Message extends string | Error = string,
105
+ Metadata extends LogMetadata = LogMetadata,
106
+ >(message: Message, metadata?: Metadata): Log<Message, Metadata> {
107
+ return this.log(message, "warn", metadata);
108
+ }
109
+
110
+ error<
111
+ Message extends string | Error = string,
112
+ Metadata extends LogMetadata = LogMetadata,
113
+ >(message: Message, metadata?: Metadata): Log<Message, Metadata> {
114
+ return this.log(message, "error", metadata);
115
+ }
116
+
117
+ get printLevel() {
118
+ return this._printLevel;
119
+ }
120
+
121
+ set printLevel(level: LogLevelSetting) {
122
+ this._printLevel = level;
123
+ }
124
+
125
+ // Info prints normally for standard user-facing logs. Debug and Warn are highlighted with a prefix. Errors print as Error instances
126
+ private formatLogMessage(message: Error | string, level: LogLevel): string {
127
+ const content = message instanceof Error ? message.message : message;
128
+ return level === "debug" || level === "warn"
129
+ ? `[${this.name} ${level.toUpperCase()}]: ${content}`
130
+ : content;
131
+ }
132
+
133
+ private _printLevel: LogLevelSetting = IS_PRODUCTION
134
+ ? "info"
135
+ : IS_TEST
7
136
  ? "silent"
8
- : process.env.NODE_ENV === "development"
9
- ? "debug"
10
- : "info",
11
- transport: {
12
- target: "pino-pretty",
13
- options: {
14
- color: true,
15
- },
16
- },
17
- });
137
+ : "debug";
138
+
139
+ private shouldPrint(level: LogLevel): boolean {
140
+ if (this.printLevel === "silent") return false;
141
+ return getLevelNumber(level) >= getLevelNumber(this.printLevel);
142
+ }
143
+ }
144
+
145
+ export const logger = createLogger("bun-workspaces");
@@ -33,6 +33,8 @@ export interface Project {
33
33
  listWorkspacesWithScript(scriptName: string): Workspace[];
34
34
  listScriptsWithWorkspaces(): Record<string, ScriptMetadata>;
35
35
  findWorkspaceByName(workspaceName: string): Workspace | null;
36
+ findWorkspaceByAlias(alias: string): Workspace | null;
37
+ findWorkspaceByNameOrAlias(nameOrAlias: string): Workspace | null;
36
38
  findWorkspacesByPattern(workspaceName: string): Workspace[];
37
39
  createScriptCommand(
38
40
  options: CreateProjectScriptCommandOptions,
@@ -41,17 +43,23 @@ export interface Project {
41
43
 
42
44
  export interface CreateProjectOptions {
43
45
  rootDir: string;
46
+ workspaceAliases?: Record<string, string>;
44
47
  }
45
48
 
46
49
  class _Project implements Project {
47
50
  public readonly rootDir: string;
51
+ public readonly workspaceAliases?: Record<string, string>;
48
52
  public readonly workspaces: Workspace[];
49
53
  public readonly name: string;
50
54
  constructor(private options: CreateProjectOptions) {
51
55
  this.rootDir = options.rootDir;
56
+ this.workspaceAliases = options.workspaceAliases;
57
+
52
58
  const { name, workspaces } = findWorkspacesFromPackage({
53
59
  rootDir: options.rootDir,
60
+ workspaceAliases: options.workspaceAliases,
54
61
  });
62
+
55
63
  this.name = name;
56
64
  this.workspaces = workspaces;
57
65
  }
@@ -70,6 +78,7 @@ class _Project implements Project {
70
78
  );
71
79
  });
72
80
  return Array.from(scripts)
81
+ .sort((a, b) => a.localeCompare(b))
73
82
  .map((name) => ({
74
83
  name,
75
84
  workspaces: this.listWorkspacesWithScript(name),
@@ -90,6 +99,20 @@ class _Project implements Project {
90
99
  );
91
100
  }
92
101
 
102
+ findWorkspaceByAlias(alias: string): Workspace | null {
103
+ return (
104
+ this.workspaces.find((workspace) => workspace.aliases.includes(alias)) ??
105
+ null
106
+ );
107
+ }
108
+
109
+ findWorkspaceByNameOrAlias(nameOrAlias: string): Workspace | null {
110
+ return (
111
+ this.findWorkspaceByName(nameOrAlias) ||
112
+ this.findWorkspaceByAlias(nameOrAlias)
113
+ );
114
+ }
115
+
93
116
  /** Accepts wildcard for finding a list of workspaces */
94
117
  findWorkspacesByPattern(workspacePattern: string): Workspace[] {
95
118
  if (!workspacePattern) return [];
@@ -100,7 +123,8 @@ class _Project implements Project {
100
123
  createScriptCommand(
101
124
  options: CreateProjectScriptCommandOptions,
102
125
  ): CreateProjectScriptCommandResult {
103
- const workspace = this.findWorkspaceByName(options.workspaceName);
126
+ const workspace = this.findWorkspaceByNameOrAlias(options.workspaceName);
127
+
104
128
  if (!workspace) {
105
129
  throw new ERRORS.ProjectWorkspaceNotFound(
106
130
  `Workspace not found: ${JSON.stringify(options.workspaceName)}`,
@@ -4,8 +4,11 @@ export const ERRORS = defineErrors(
4
4
  "PackageNotFound",
5
5
  "InvalidPackageJson",
6
6
  "DuplicateWorkspaceName",
7
+ "InvalidWorkspaceName",
7
8
  "NoWorkspaceName",
8
9
  "InvalidScripts",
9
10
  "InvalidWorkspaces",
10
11
  "InvalidWorkspacePattern",
12
+ "AliasConflict",
13
+ "AliasedWorkspaceNotFound",
11
14
  );
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { Glob } from "bun";
3
+ import type { ProjectConfig } from "../config";
4
4
  import { logger } from "../internal/logger";
5
5
  import { ERRORS } from "./errors";
6
6
  import {
@@ -13,6 +13,7 @@ import type { Workspace } from "./workspace";
13
13
  export interface FindWorkspacesOptions {
14
14
  rootDir: string;
15
15
  workspaceGlobs: string[];
16
+ workspaceAliases?: ProjectConfig["workspaceAliases"];
16
17
  }
17
18
 
18
19
  const validatePattern = (pattern: string) => {
@@ -44,6 +45,7 @@ const validateWorkspace = (workspace: Workspace, workspaces: Workspace[]) => {
44
45
  export const findWorkspaces = ({
45
46
  rootDir,
46
47
  workspaceGlobs,
48
+ workspaceAliases,
47
49
  }: FindWorkspacesOptions) => {
48
50
  rootDir = path.resolve(rootDir);
49
51
 
@@ -52,8 +54,7 @@ export const findWorkspaces = ({
52
54
  for (const pattern of workspaceGlobs) {
53
55
  if (!validatePattern(pattern)) continue;
54
56
 
55
- const glob = new Glob(pattern);
56
- for (const item of scanWorkspaceGlob(glob, rootDir)) {
57
+ for (const item of scanWorkspaceGlob(pattern, rootDir)) {
57
58
  const packageJsonPath = resolvePackageJsonPath(item);
58
59
  if (packageJsonPath) {
59
60
  const packageJsonContent = resolvePackageJsonContent(
@@ -67,6 +68,9 @@ export const findWorkspaces = ({
67
68
  matchPattern: pattern,
68
69
  path: path.relative(rootDir, path.dirname(packageJsonPath)),
69
70
  packageJson: packageJsonContent,
71
+ aliases: Object.entries(workspaceAliases ?? {})
72
+ .filter(([_, value]) => value === packageJsonContent.name)
73
+ .map(([key]) => key),
70
74
  };
71
75
 
72
76
  if (validateWorkspace(workspace, workspaces)) {
@@ -83,13 +87,30 @@ export const findWorkspaces = ({
83
87
  return { workspaces };
84
88
  };
85
89
 
86
- export interface FindWorkspacesFromPackageOptions {
87
- rootDir: string;
88
- }
90
+ export const validateWorkspaceAliases = (
91
+ workspaces: Workspace[],
92
+ workspaceAliases: ProjectConfig["workspaceAliases"],
93
+ ) => {
94
+ for (const [alias, name] of Object.entries(workspaceAliases ?? {})) {
95
+ if (workspaces.find((ws) => ws.name === alias)) {
96
+ throw new ERRORS.AliasConflict(
97
+ `Alias ${JSON.stringify(alias)} conflicts with workspace name ${JSON.stringify(name)}`,
98
+ );
99
+ }
100
+ if (!workspaces.find((ws) => ws.name === name)) {
101
+ throw new ERRORS.AliasedWorkspaceNotFound(
102
+ `Workspace ${JSON.stringify(name)} was aliased by ${JSON.stringify(
103
+ alias,
104
+ )} but was not found`,
105
+ );
106
+ }
107
+ }
108
+ };
89
109
 
90
110
  export const findWorkspacesFromPackage = ({
91
111
  rootDir,
92
- }: FindWorkspacesFromPackageOptions) => {
112
+ workspaceAliases,
113
+ }: ProjectConfig & { rootDir: string }) => {
93
114
  const packageJsonPath = path.join(rootDir, "package.json");
94
115
  if (!fs.existsSync(packageJsonPath)) {
95
116
  throw new ERRORS.PackageNotFound(
@@ -101,11 +122,16 @@ export const findWorkspacesFromPackage = ({
101
122
  "workspaces",
102
123
  ]);
103
124
 
125
+ const result = findWorkspaces({
126
+ rootDir,
127
+ workspaceGlobs: packageJson.workspaces ?? [],
128
+ workspaceAliases,
129
+ });
130
+
131
+ validateWorkspaceAliases(result.workspaces, workspaceAliases);
132
+
104
133
  return {
105
- ...findWorkspaces({
106
- rootDir,
107
- workspaceGlobs: packageJson.workspaces ?? [],
108
- }),
134
+ ...result,
109
135
  name: packageJson.name ?? "",
110
136
  };
111
137
  };
@@ -1,7 +1,6 @@
1
1
  export {
2
2
  findWorkspaces,
3
3
  findWorkspacesFromPackage,
4
- type FindWorkspacesFromPackageOptions,
5
4
  type FindWorkspacesOptions,
6
5
  } from "./findWorkspaces";
7
6
  export { type Workspace } from "./workspace";
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { Glob } from "bun";
3
+ import { Glob } from "glob";
4
4
  import { logger } from "../internal/logger";
5
5
  import { ERRORS } from "./errors";
6
6
 
@@ -22,12 +22,8 @@ export type ResolvedPackageJsonContent = {
22
22
 
23
23
  type UnknownPackageJson = Record<string, unknown>;
24
24
 
25
- export const scanWorkspaceGlob = (glob: Glob, rootDir: string) =>
26
- glob.scanSync({
27
- cwd: rootDir,
28
- onlyFiles: false,
29
- absolute: true,
30
- });
25
+ export const scanWorkspaceGlob = (pattern: string, rootDir: string) =>
26
+ new Glob(pattern, { absolute: true, cwd: rootDir }).iterateSync();
31
27
 
32
28
  const validateJsonRoot = (json: UnknownPackageJson) => {
33
29
  if (!json || typeof json !== "object" || Array.isArray(json)) {
@@ -46,6 +42,18 @@ const validateName = (json: UnknownPackageJson) => {
46
42
  );
47
43
  }
48
44
 
45
+ if (!json.name.trim()) {
46
+ throw new ERRORS.NoWorkspaceName(
47
+ `Expected package.json to have a non-empty "name" field`,
48
+ );
49
+ }
50
+
51
+ if (json.name.includes("*")) {
52
+ throw new ERRORS.InvalidWorkspaceName(
53
+ `Package name cannot contain the character '*' (workspace: "${json.name}")`,
54
+ );
55
+ }
56
+
49
57
  return json.name;
50
58
  };
51
59
 
@@ -133,7 +141,7 @@ export const resolvePackageJsonContent = (
133
141
  try {
134
142
  json = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
135
143
  } catch (error) {
136
- logger.error(error);
144
+ logger.error(error as Error);
137
145
  throw new ERRORS.InvalidPackageJson(
138
146
  `Failed to read and parse package.json at ${packageJsonPath}: ${
139
147
  (error as Error).message
@@ -9,4 +9,6 @@ export interface Workspace {
9
9
  matchPattern: string;
10
10
  /** The contents of the workspace's package.json, with `"workspaces"` and `"scripts"` resolved */
11
11
  packageJson: ResolvedPackageJsonContent;
12
+ /** Aliases assigned to the workspace via the `"workspaceAliases"` field in the config */
13
+ aliases: string[];
12
14
  }
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  // Enable latest features
4
- "lib": ["ESNext", "DOM"],
4
+ "lib": ["ESNext", "DOM", "DOM.AsyncIterable", "DOM.Iterable"],
5
5
  "target": "ESNext",
6
6
  "module": "ESNext",
7
7
  "moduleDetection": "force",
@@ -1,12 +0,0 @@
1
- {
2
- "recommendations": [
3
- "esbenp.prettier-vscode",
4
- "oven.bun-vscode",
5
- "streetsidesoftware.code-spell-checker",
6
- "jasonnutter.vscode-codeowners",
7
- "EditorConfig.EditorConfig",
8
- "dbaeumer.vscode-eslint",
9
- "github.vscode-github-actions",
10
- "aaron-bond.better-comments"
11
- ]
12
- }
@@ -1,23 +0,0 @@
1
- {
2
- "editor.codeActionsOnSave": {
3
- "source.fixAll.eslint": "explicit"
4
- },
5
- "eslint.codeActionsOnSave.rules": ["import/order"],
6
- "eslint.validate": ["javascript", "typescript"],
7
- "[typescript]": {
8
- "editor.formatOnSave": true,
9
- "editor.defaultFormatter": "esbenp.prettier-vscode"
10
- },
11
- "[javascript]": {
12
- "editor.formatOnSave": true,
13
- "editor.defaultFormatter": "esbenp.prettier-vscode"
14
- },
15
- "[json]": {
16
- "editor.formatOnSave": true,
17
- "editor.defaultFormatter": "esbenp.prettier-vscode"
18
- },
19
- "[jsonc]": {
20
- "editor.formatOnSave": true,
21
- "editor.defaultFormatter": "esbenp.prettier-vscode"
22
- }
23
- }
package/eslint.config.mjs DELETED
@@ -1,45 +0,0 @@
1
- import js from "@eslint/js";
2
- import importPlugin from "eslint-plugin-import";
3
- import typescriptEslint from "typescript-eslint";
4
-
5
- export default [
6
- ...typescriptEslint.config(
7
- js.configs.recommended,
8
- typescriptEslint.configs.recommended,
9
- ),
10
- {
11
- plugins: {
12
- import: importPlugin,
13
- },
14
- rules: {
15
- "@typescript-eslint/no-empty-interface": "off",
16
- "@typescript-eslint/no-empty-function": "off",
17
- "no-empty": "warn",
18
- "@typescript-eslint/no-extra-semi": "off",
19
- "@typescript-eslint/no-explicit-any": "off",
20
-
21
- "@typescript-eslint/no-unused-vars": [
22
- "warn",
23
- {
24
- varsIgnorePattern: "^_",
25
- argsIgnorePattern: "^_",
26
- destructuredArrayIgnorePattern: "^_",
27
- caughtErrorsIgnorePattern: "^_",
28
- },
29
- ],
30
-
31
- eqeqeq: "error",
32
- "prefer-const": "error",
33
-
34
- "import/order": [
35
- "warn",
36
- {
37
- alphabetize: {
38
- order: "asc",
39
- caseInsensitive: true,
40
- },
41
- },
42
- ],
43
- },
44
- },
45
- ];
package/src/cli/output.ts DELETED
@@ -1,6 +0,0 @@
1
- import { IS_TEST } from "../internal/env";
2
-
3
- export const OUTPUT_CONFIG = {
4
- writeOut: (s: string) => !IS_TEST && process.stdout.write(s),
5
- writeErr: (s: string) => !IS_TEST && process.stderr.write(s),
6
- };