@storm-software/eslint-plugin-bun 0.0.2

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.
@@ -0,0 +1,48 @@
1
+ interface BunWorkspacePackageJson {
2
+ catalog?: Record<string, string>;
3
+ catalogs?: Record<string, Record<string, string>>;
4
+ workspaces?: string[] | {
5
+ packages?: string[];
6
+ catalog?: Record<string, string>;
7
+ catalogs?: Record<string, Record<string, string>>;
8
+ };
9
+ [key: string]: unknown;
10
+ }
11
+ type CatalogName = "default" | (string & {});
12
+ interface SpecifierConflicts {
13
+ conflicts: boolean;
14
+ newCatalogName: string;
15
+ existingSpecifier?: string;
16
+ }
17
+ interface BunWorkspace {
18
+ filepath: string;
19
+ setContent: (content: string) => void;
20
+ hasChanged: () => boolean;
21
+ hasQueue: () => boolean;
22
+ clearQueue: () => void;
23
+ queueChange: (fn: (workspace: BunWorkspace) => void, order?: "pre" | "post") => void;
24
+ toJSON: () => BunWorkspacePackageJson;
25
+ toString: () => string;
26
+ setPackage: (catalog: CatalogName, packageName: string, specifier: string) => void;
27
+ setPackageNoConflicts: (catalog: CatalogName, packageName: string, specifier: string) => void;
28
+ getPackageCatalogs: (packageName: string) => string[];
29
+ hasSpecifierConflicts: (catalog: CatalogName, packageName: string, specifier: string) => SpecifierConflicts;
30
+ getCatalogData: () => {
31
+ catalog?: Record<string, string>;
32
+ catalogs?: Record<string, Record<string, string>>;
33
+ };
34
+ getWorkspacePackagePatterns: () => string[];
35
+ }
36
+ /**
37
+ * Find and parse the Bun workspace root `package.json` that holds catalogs.
38
+ *
39
+ * Catalogs live in root `package.json` (top-level or under `workspaces`), not
40
+ * `pnpm-workspace.yaml`. See https://bun.com/docs/pm/catalogs
41
+ */
42
+ declare function readBunWorkspace(cwd?: string): BunWorkspace;
43
+ declare function getBunWorkspace(cwd?: string): BunWorkspace;
44
+ declare function resetBunWorkspaceCache(): void;
45
+ declare function isRootWorkspacePackageJson(filename: string, cwd?: string): boolean;
46
+ declare function collectWorkspacePackageJsonPaths(workspaceRoot: BunWorkspace): string[];
47
+
48
+ export { type BunWorkspace, type BunWorkspacePackageJson, type CatalogName, type SpecifierConflicts, collectWorkspacePackageJsonPaths, getBunWorkspace, isRootWorkspacePackageJson, readBunWorkspace, resetBunWorkspaceCache };
@@ -0,0 +1,265 @@
1
+ import fs from 'node:fs';
2
+ import process from 'node:process';
3
+ import { findUpSync } from 'find-up-simple';
4
+ import { dirname, normalize, resolve } from 'pathe';
5
+ import { globSync } from 'tinyglobby';
6
+
7
+ // src/helpers/bun-workspace.ts
8
+ function isWorkspaceObject(workspaces) {
9
+ return Boolean(workspaces) && typeof workspaces === "object" && !Array.isArray(workspaces);
10
+ }
11
+ function detectCatalogRoot(data) {
12
+ if (data.catalog || data.catalogs) {
13
+ return "top";
14
+ }
15
+ if (isWorkspaceObject(data.workspaces) && (data.workspaces.catalog || data.workspaces.catalogs)) {
16
+ return "workspaces";
17
+ }
18
+ return isWorkspaceObject(data.workspaces) ? "workspaces" : "top";
19
+ }
20
+ function normalizeCatalogName(name) {
21
+ return name.replace(/\^/g, "h").replace(/~/g, "t").replace(/\./g, "_").replace(/</g, "l").replace(/>/g, "g").replace(/=/g, "e").replace(/\*/g, "s").replace(/@/g, "a").replace(/\|/g, "p").replace(/&/g, "n").replace(/-/g, "m").replace(/\+/g, "u").replace(/\s/g, "_");
22
+ }
23
+ function sortObjectKeys(input) {
24
+ return Object.fromEntries(
25
+ Object.entries(input).sort(([left], [right]) => left.localeCompare(right))
26
+ );
27
+ }
28
+ function isWorkspaceRootPackageJson(data) {
29
+ return Boolean(
30
+ data.workspaces || data.catalog || data.catalogs
31
+ );
32
+ }
33
+ function readBunWorkspace(cwd = process.cwd()) {
34
+ const filepath = findUpSync("package.json", {
35
+ cwd,
36
+ type: "file"
37
+ });
38
+ if (!filepath) {
39
+ throw new Error("package.json not found");
40
+ }
41
+ let current = filepath;
42
+ let content = fs.readFileSync(current, "utf8");
43
+ let data = JSON.parse(content);
44
+ while (!isWorkspaceRootPackageJson(data)) {
45
+ const searchFrom = dirname(dirname(current));
46
+ const next = findUpSync("package.json", {
47
+ cwd: searchFrom,
48
+ type: "file"
49
+ });
50
+ if (!next || normalize(next) === normalize(current)) {
51
+ break;
52
+ }
53
+ current = next;
54
+ content = fs.readFileSync(current, "utf8");
55
+ data = JSON.parse(content);
56
+ }
57
+ if (!isWorkspaceRootPackageJson(data)) {
58
+ throw new Error(
59
+ "Bun workspace root package.json with workspaces/catalog not found"
60
+ );
61
+ }
62
+ let hasChanged = false;
63
+ let queueTimer;
64
+ const queue = [];
65
+ let catalogRoot = detectCatalogRoot(data);
66
+ const write = () => {
67
+ if (!fs.existsSync(current)) {
68
+ return;
69
+ }
70
+ fs.writeFileSync(current, `${JSON.stringify(data, null, 2)}
71
+ `);
72
+ };
73
+ const clearQueue = () => {
74
+ if (queueTimer != null) {
75
+ clearTimeout(queueTimer);
76
+ queueTimer = void 0;
77
+ }
78
+ queue.length = 0;
79
+ };
80
+ const getCatalogContainer = () => {
81
+ if (catalogRoot === "workspaces") {
82
+ if (!isWorkspaceObject(data.workspaces)) {
83
+ data.workspaces = {
84
+ packages: Array.isArray(data.workspaces) ? data.workspaces : [],
85
+ catalog: {},
86
+ catalogs: {}
87
+ };
88
+ }
89
+ return data.workspaces;
90
+ }
91
+ return data;
92
+ };
93
+ const getCatalogData = () => {
94
+ const container = getCatalogContainer();
95
+ return {
96
+ catalog: container.catalog,
97
+ catalogs: container.catalogs
98
+ };
99
+ };
100
+ const setPackage = (catalogName, packageName, specifier) => {
101
+ const container = getCatalogContainer();
102
+ if (catalogName === "default") {
103
+ container.catalog ??= {};
104
+ if (container.catalog[packageName] !== specifier) {
105
+ container.catalog[packageName] = specifier;
106
+ container.catalog = sortObjectKeys(container.catalog);
107
+ hasChanged = true;
108
+ }
109
+ return;
110
+ }
111
+ container.catalogs ??= {};
112
+ container.catalogs[catalogName] ??= {};
113
+ if (container.catalogs[catalogName][packageName] !== specifier) {
114
+ container.catalogs[catalogName][packageName] = specifier;
115
+ container.catalogs[catalogName] = sortObjectKeys(
116
+ container.catalogs[catalogName]
117
+ );
118
+ container.catalogs = sortObjectKeys(container.catalogs);
119
+ hasChanged = true;
120
+ }
121
+ };
122
+ const hasSpecifierConflicts = (catalogName, packageName, specifier) => {
123
+ const { catalog, catalogs } = getCatalogData();
124
+ const existingSpecifier = catalogName === "default" ? catalog?.[packageName] : catalogs?.[catalogName]?.[packageName];
125
+ if (existingSpecifier === specifier) {
126
+ return {
127
+ conflicts: false,
128
+ newCatalogName: catalogName,
129
+ existingSpecifier
130
+ };
131
+ }
132
+ if (existingSpecifier) {
133
+ const versionSuffix = normalizeCatalogName(specifier);
134
+ const newCatalogName = catalogName === "default" ? `conflicts_${packageName}_${versionSuffix}` : `conflicts_${catalogName}_${versionSuffix}`;
135
+ return {
136
+ conflicts: true,
137
+ existingSpecifier,
138
+ newCatalogName
139
+ };
140
+ }
141
+ return {
142
+ conflicts: false,
143
+ newCatalogName: catalogName
144
+ };
145
+ };
146
+ const getPackageCatalogs = (packageName) => {
147
+ const found = [];
148
+ const { catalog, catalogs } = getCatalogData();
149
+ if (catalogs) {
150
+ for (const name of Object.keys(catalogs)) {
151
+ if (catalogs[name]?.[packageName]) {
152
+ found.push(name);
153
+ }
154
+ }
155
+ }
156
+ if (catalog?.[packageName]) {
157
+ found.push("default");
158
+ }
159
+ return found;
160
+ };
161
+ const workspace2 = {
162
+ filepath: current,
163
+ setContent(newContent) {
164
+ content = newContent;
165
+ data = JSON.parse(newContent);
166
+ catalogRoot = detectCatalogRoot(data);
167
+ hasChanged = false;
168
+ },
169
+ hasChanged: () => hasChanged,
170
+ hasQueue: () => queueTimer != null,
171
+ clearQueue,
172
+ queueChange(fn, order) {
173
+ if (order === "pre") {
174
+ queue.unshift(fn);
175
+ } else {
176
+ queue.push(fn);
177
+ }
178
+ if (queueTimer != null) {
179
+ clearTimeout(queueTimer);
180
+ }
181
+ queueTimer = setTimeout(() => {
182
+ queueTimer = void 0;
183
+ const clone = [...queue];
184
+ queue.length = 0;
185
+ for (const queued of clone) {
186
+ queued(workspace2);
187
+ }
188
+ if (hasChanged) {
189
+ write();
190
+ }
191
+ }, 1e3);
192
+ },
193
+ toJSON: () => data,
194
+ toString: () => `${JSON.stringify(data, null, 2)}
195
+ `,
196
+ setPackage,
197
+ setPackageNoConflicts(catalogName, packageName, specifier) {
198
+ const { newCatalogName } = hasSpecifierConflicts(
199
+ catalogName,
200
+ packageName,
201
+ specifier
202
+ );
203
+ setPackage(newCatalogName, packageName, specifier);
204
+ },
205
+ getPackageCatalogs,
206
+ hasSpecifierConflicts,
207
+ getCatalogData,
208
+ getWorkspacePackagePatterns() {
209
+ if (Array.isArray(data.workspaces)) {
210
+ return data.workspaces;
211
+ }
212
+ if (isWorkspaceObject(data.workspaces) && data.workspaces.packages) {
213
+ return data.workspaces.packages;
214
+ }
215
+ return [];
216
+ }
217
+ };
218
+ return workspace2;
219
+ }
220
+ var WORKSPACE_CACHE_TIME = 1e4;
221
+ var workspaceLastRead;
222
+ var workspace;
223
+ function getBunWorkspace(cwd = process.cwd()) {
224
+ if (workspaceLastRead && workspace && !workspace.hasQueue() && Date.now() - workspaceLastRead > WORKSPACE_CACHE_TIME) {
225
+ workspace = void 0;
226
+ }
227
+ if (!workspace) {
228
+ workspace = readBunWorkspace(cwd);
229
+ workspaceLastRead = Date.now();
230
+ }
231
+ return workspace;
232
+ }
233
+ function resetBunWorkspaceCache() {
234
+ workspace?.clearQueue();
235
+ workspace = void 0;
236
+ workspaceLastRead = void 0;
237
+ }
238
+ function isRootWorkspacePackageJson(filename, cwd = process.cwd()) {
239
+ try {
240
+ const root = getBunWorkspace(cwd);
241
+ return normalize(root.filepath) === normalize(filename);
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+ function collectWorkspacePackageJsonPaths(workspaceRoot) {
247
+ const root = resolve(dirname(workspaceRoot.filepath));
248
+ const patterns = workspaceRoot.getWorkspacePackagePatterns();
249
+ const dirs = patterns.length ? globSync(patterns, {
250
+ cwd: root,
251
+ dot: false,
252
+ ignore: [
253
+ "**/node_modules/**",
254
+ "**/dist/**",
255
+ "**/build/**"
256
+ ],
257
+ absolute: true,
258
+ expandDirectories: false,
259
+ onlyDirectories: true
260
+ }) : [];
261
+ dirs.push(root);
262
+ return dirs.map((dir) => resolve(dir, "package.json")).filter((path) => fs.existsSync(path)).sort();
263
+ }
264
+
265
+ export { collectWorkspacePackageJsonPaths, getBunWorkspace, isRootWorkspacePackageJson, readBunWorkspace, resetBunWorkspaceCache };
@@ -0,0 +1,9 @@
1
+ import { RuleWithMetaAndName } from '@typescript-eslint/utils/eslint-utils';
2
+ import { Rule } from 'eslint';
3
+
4
+ type RuleModule<T extends readonly unknown[]> = Rule.RuleModule & {
5
+ defaultOptions: T;
6
+ };
7
+ declare const createRule: <TOptions extends readonly unknown[], TMessageIds extends string>({ name, meta, ...rule }: Readonly<RuleWithMetaAndName<TOptions, TMessageIds>>) => RuleModule<TOptions>;
8
+
9
+ export { type RuleModule, createRule };
@@ -0,0 +1,42 @@
1
+ import defu from 'defu';
2
+
3
+ // src/helpers/create-rule.ts
4
+ function RuleCreator(urlCreator) {
5
+ return function createNamedRule({
6
+ name,
7
+ meta,
8
+ ...rule
9
+ }) {
10
+ return createRuleInfo({
11
+ meta: {
12
+ ...meta,
13
+ docs: {
14
+ ...meta.docs,
15
+ url: urlCreator(name)
16
+ }
17
+ },
18
+ ...rule
19
+ });
20
+ };
21
+ }
22
+ function createRuleInfo({
23
+ create,
24
+ defaultOptions,
25
+ meta
26
+ }) {
27
+ return {
28
+ create: ((context) => {
29
+ const optionsWithDefault = context.options.map((options, index) => {
30
+ return defu(defaultOptions?.[index] ?? {}, options);
31
+ });
32
+ return create(context, optionsWithDefault);
33
+ }),
34
+ defaultOptions: defaultOptions ?? {},
35
+ meta
36
+ };
37
+ }
38
+ var createRule = RuleCreator(
39
+ (ruleName) => `https://docs.stormsoftware.com/projects/storm-ops/eslint-plugin-bun/rules/${ruleName}.md`
40
+ );
41
+
42
+ export { createRule };
@@ -0,0 +1,11 @@
1
+ import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
2
+
3
+ declare function getPackageJsonRootNode(context: RuleContext<string, unknown[]>): any | undefined;
4
+ declare function iterateDependencies(context: RuleContext<string, unknown[]>, fields: string[]): Generator<{
5
+ packageName: string;
6
+ specifier: string;
7
+ property: any;
8
+ }, void, unknown>;
9
+ declare function getObjectPath(obj: Record<string, unknown>, path: string[]): Record<string, string> | undefined;
10
+
11
+ export { getObjectPath, getPackageJsonRootNode, iterateDependencies };
@@ -0,0 +1,63 @@
1
+ // src/helpers/iterate-dependencies.ts
2
+ function getPackageJsonRootNode(context) {
3
+ if (!context.filename.endsWith("package.json")) {
4
+ return void 0;
5
+ }
6
+ const ast = context.sourceCode.ast;
7
+ const root = ast.body[0];
8
+ if (root?.expression?.type === "JSONObjectExpression") {
9
+ return root.expression;
10
+ }
11
+ return void 0;
12
+ }
13
+ function* iterateDependencies(context, fields) {
14
+ const root = getPackageJsonRootNode(context);
15
+ if (!root) {
16
+ return;
17
+ }
18
+ for (const fieldName of fields) {
19
+ const path = fieldName.split(".");
20
+ let node = root;
21
+ for (const segment of path) {
22
+ const item = node.properties?.find(
23
+ (property) => property.key.type === "JSONLiteral" && property.key.value === segment
24
+ );
25
+ if (!item?.value || item.value.type !== "JSONObjectExpression") {
26
+ node = void 0;
27
+ break;
28
+ }
29
+ node = item.value;
30
+ }
31
+ if (!node || node === root || !node.properties) {
32
+ continue;
33
+ }
34
+ for (const property of node.properties) {
35
+ if (property.value.type !== "JSONLiteral" || property.key.type !== "JSONLiteral") {
36
+ continue;
37
+ }
38
+ if (typeof property.value.value !== "string") {
39
+ continue;
40
+ }
41
+ yield {
42
+ packageName: String(property.key.value),
43
+ specifier: String(property.value.value),
44
+ property
45
+ };
46
+ }
47
+ }
48
+ }
49
+ function getObjectPath(obj, path) {
50
+ let current = obj;
51
+ for (const key of path) {
52
+ if (!current || typeof current !== "object") {
53
+ return void 0;
54
+ }
55
+ current = current[key];
56
+ if (!current) {
57
+ return void 0;
58
+ }
59
+ }
60
+ return current;
61
+ }
62
+
63
+ export { getObjectPath, getPackageJsonRootNode, iterateDependencies };
@@ -0,0 +1,58 @@
1
+ import { RuleModule } from './helpers/create-rule.js';
2
+ import { Linter, ESLint } from 'eslint';
3
+ export { configs } from './configs/index.js';
4
+ import { plugin } from './plugin.js';
5
+ import '@typescript-eslint/utils/eslint-utils';
6
+
7
+ /**
8
+ * ESLint plugin for Storm Software to enforce Bun catalog usage across monorepo packages.
9
+ *
10
+ * Catalog versions are stored in the workspace root `package.json` (`catalog` /
11
+ * `catalogs`, top-level or under `workspaces`), matching Bun's catalog protocol:
12
+ * https://bun.com/docs/pm/catalogs
13
+ *
14
+ * @packageDocumentation
15
+ *
16
+ * @example
17
+ * ```js
18
+ * import bun from "@storm-software/eslint-plugin-bun";
19
+ *
20
+ * export default [
21
+ * ...bun.configs.recommended,
22
+ * ];
23
+ * ```
24
+ *
25
+ * @see [Storm Software](https://stormsoftware.com) for more information about our projects and licensing.
26
+ */
27
+ declare const eslintPlugin: ESLint.Plugin;
28
+
29
+ declare const rules: {
30
+ "json-enforce-catalog": RuleModule<[{
31
+ allowedProtocols?: string[];
32
+ autofix?: boolean;
33
+ defaultCatalog?: string;
34
+ reuseExistingCatalog?: boolean;
35
+ conflicts?: "new-catalog" | "overrides" | "error";
36
+ fields?: string[];
37
+ ignore?: string[];
38
+ }]>;
39
+ "json-valid-catalog": RuleModule<[{
40
+ autoInsert?: boolean;
41
+ autoInsertDefaultSpecifier?: string;
42
+ autofix?: boolean;
43
+ fields?: string[];
44
+ }]>;
45
+ "json-no-unused-catalog-item": RuleModule<[]>;
46
+ "json-no-duplicate-catalog-item": RuleModule<[{
47
+ allow?: string[];
48
+ }]>;
49
+ };
50
+ type RuleDefinitions = (typeof plugin)["rules"];
51
+ type RuleOptions = {
52
+ [K in keyof RuleDefinitions]: RuleDefinitions[K]["defaultOptions"];
53
+ };
54
+ type Rules = {
55
+ [K in keyof RuleOptions]: Linter.RuleEntry<RuleOptions[K]>;
56
+ };
57
+
58
+ export { type RuleOptions, type Rules, eslintPlugin as default, rules };