@murky-web/typebuddy 0.1.0

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,296 @@
1
+ //#region src/type_helper.ts
2
+ const EMPTY_LENGTH = 0;
3
+ const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
4
+ const ulidRegex = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
5
+ function cloneDefaultArray(defaultValue) {
6
+ if (!defaultValue) return defaultValue;
7
+ return [...defaultValue];
8
+ }
9
+ /**
10
+ * Checks if the provided result is a `Success` type.
11
+ *
12
+ * @template T The type of the value contained in the `Success` type.
13
+ * @param {Readonly<Success<T> | Failed>} result The result to check, which can
14
+ * be either a `Success<T>` or `Failed`.
15
+ * @returns {boolean} `true` if the result is a `Success<T>`, `false` if it is a `Failed`.
16
+ * This function acts as a type guard, narrowing the type of `result`
17
+ * to `Success<T>` when the condition is met.
18
+ * @example
19
+ * ```typescript
20
+ * const result = await someAsyncFunction();
21
+ * if (isSuccess(result)) {
22
+ * return result.value; // TypeScript knows `value` is of type `T`
23
+ * }
24
+ * ```
25
+ */
26
+ function isSuccess(result) {
27
+ return !result.isError;
28
+ }
29
+ function isString(value) {
30
+ return typeof value === "string";
31
+ }
32
+ /**
33
+ * Parses the input value as a boolean. Returns false if the value is no string.
34
+ * @param {unknown} value - The value to check.
35
+ * @returns {boolean} True if the value is a string.
36
+ */
37
+ function isEmptyString(value) {
38
+ if (!isString(value)) return false;
39
+ return value.trim() === "";
40
+ }
41
+ /**
42
+ * Returns true if the value is null.
43
+ * @param {unknown} value - The value to check.
44
+ * @returns {boolean} True if the value is null.
45
+ */
46
+ function isNull(value) {
47
+ return value === null;
48
+ }
49
+ /**
50
+ * Returns true if the value is undefined.
51
+ * @param {unknown} value - The value to check.
52
+ * @returns {boolean} True if the value is undefined.
53
+ */
54
+ function isUndefined(value) {
55
+ return typeof value === "undefined";
56
+ }
57
+ /**
58
+ * Checks whether an Optional value is currently in its missing state.
59
+ * @param {Optional<T>} value - The Optional value to check.
60
+ * @returns {boolean} True when the value is undefined.
61
+ */
62
+ function isOptional(value) {
63
+ return isUndefined(value);
64
+ }
65
+ /**
66
+ * Checks whether a Maybe value is currently in its missing state.
67
+ * @param {Maybe<T>} value - The Maybe value to check.
68
+ * @returns {boolean} True when the value is null.
69
+ */
70
+ function isMaybe(value) {
71
+ return isNull(value);
72
+ }
73
+ /**
74
+ * Checks whether a Nullable value is currently in its missing state.
75
+ * @param {Nullable<T>} value - The Nullable value to check.
76
+ * @returns {boolean} True when the value is null or undefined.
77
+ */
78
+ function isNullable(value) {
79
+ return isNull(value) || isUndefined(value);
80
+ }
81
+ function isArray(value) {
82
+ return Array.isArray(value);
83
+ }
84
+ function isEmptyArray(value) {
85
+ return Array.isArray(value) && value.length === EMPTY_LENGTH;
86
+ }
87
+ function fastIsArray(value) {
88
+ return Object.prototype.toString.call(value) === "[object Array]";
89
+ }
90
+ function isNumber(value) {
91
+ if (typeof value === "string" && value.trim() === "") return false;
92
+ return typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value);
93
+ }
94
+ function isObject(value) {
95
+ if (typeof value !== "object" || value === null || isArray(value) || Object.prototype.toString.call(value) !== "[object Object]") return false;
96
+ const objectValue = value;
97
+ return Object.getPrototypeOf(objectValue) === Object.prototype;
98
+ }
99
+ function isBoolean(value) {
100
+ return typeof value === "boolean";
101
+ }
102
+ function isFunction(value) {
103
+ return typeof value === "function";
104
+ }
105
+ function isPromise(value) {
106
+ if (typeof value !== "object" || value === null) return false;
107
+ return typeof Reflect.get(value, "then") === "function";
108
+ }
109
+ function isError(value) {
110
+ return value instanceof Error;
111
+ }
112
+ function isDate(value) {
113
+ return value instanceof Date;
114
+ }
115
+ function isRegExp(value) {
116
+ return value instanceof RegExp;
117
+ }
118
+ function isSymbol(value) {
119
+ return typeof value === "symbol";
120
+ }
121
+ function isEmptyObject(value) {
122
+ return typeof value === "object" && !isNull(value) && !isUndefined(value) && !isEmptyArray(value) && Object.getPrototypeOf(value) === Object.prototype && Object.keys(value).length === EMPTY_LENGTH;
123
+ }
124
+ /**
125
+ * Check if a value is an instance of a class.
126
+ * @param {unknown} value - The value to check.
127
+ * @param {unknown} constructor - The class constructor to check against.
128
+ * @returns {boolean} True if the value is an instance of the class.
129
+ */
130
+ function isInstanceOf(value, constructor) {
131
+ return value instanceof constructor;
132
+ }
133
+ /**
134
+ * Get the keys of an object.
135
+ * @param {unknown} object - The object to get the keys of.
136
+ * @returns {Array} Keys of object.
137
+ */
138
+ function getKeys(object) {
139
+ return Object.keys(object);
140
+ }
141
+ function parseInteger(value, defaultValue) {
142
+ if (isNumber(value)) return Math.floor(value);
143
+ if (isString(value)) {
144
+ const parsed = Number(value.trim());
145
+ if (Number.isInteger(parsed)) return parsed;
146
+ }
147
+ return defaultValue;
148
+ }
149
+ /**
150
+ * Check if a value is an integer.
151
+ * @param {unknown} value - The value to check.
152
+ * @returns {boolean} True if the value is an integer.
153
+ */
154
+ function isInteger(value) {
155
+ return typeof value === "number" && Number.isInteger(value);
156
+ }
157
+ /**
158
+ * Check if a value is a float.
159
+ * @param {unknown} value - The value to check.
160
+ * @returns {boolean} True if the value is a float.
161
+ */
162
+ function isFloat(value) {
163
+ return typeof value === "number" && !Number.isNaN(value) && !Number.isInteger(value);
164
+ }
165
+ function parseFloat(value, defaultValue) {
166
+ if (isNumber(value)) return value;
167
+ if (isString(value)) {
168
+ const normalizedValue = value.trim().replace(",", ".");
169
+ const parsed = Number.parseFloat(normalizedValue);
170
+ if (!Number.isNaN(parsed)) return parsed;
171
+ }
172
+ return defaultValue;
173
+ }
174
+ function parseNumber(value, defaultValue) {
175
+ if (isNumber(value)) return value;
176
+ if (isString(value)) {
177
+ const normalizedValue = value.trim().replace(",", ".");
178
+ const parsed = Number(normalizedValue);
179
+ if (Number.isFinite(parsed)) return parsed;
180
+ }
181
+ return defaultValue;
182
+ }
183
+ /**
184
+ * Parses the input value as a string. Returns an empty string if the
185
+ * value cannot be converted.
186
+ * @param {unknown} value - The value to check.
187
+ * @param {Optional<string>} defaultValue - Value gets returned if string could not
188
+ * be parsed.
189
+ * @returns {string} The parsed string.
190
+ */
191
+ function parseString(value, defaultValue = "") {
192
+ if (isString(value)) return value;
193
+ if (isNumber(value)) return value.toString();
194
+ if (typeof value === "boolean") return value.toString();
195
+ return defaultValue;
196
+ }
197
+ /**
198
+ * Returns true for values that behave like "empty" application input.
199
+ * @param {unknown} value - The value to check.
200
+ * @returns {boolean} True if the value is nullish, empty string, empty array,
201
+ * false, or a plain object whose values are all empty-like.
202
+ */
203
+ function isEmptyLike(value) {
204
+ if (isNull(value) || isUndefined(value)) return true;
205
+ if (isString(value)) return isEmptyString(value);
206
+ if (isArray(value)) return value.every((entry) => {
207
+ return isEmptyLike(entry);
208
+ });
209
+ if (isBoolean(value)) return !value;
210
+ if (isObject(value)) {
211
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
212
+ return Object.values(value).every((entry) => {
213
+ return isEmptyLike(entry);
214
+ });
215
+ }
216
+ return false;
217
+ }
218
+ /**
219
+ * Checks if the provided value contains empty values.
220
+ *
221
+ * This function determines if the given value is either an empty string,
222
+ * an empty object, or a string representation of an empty object.
223
+ *
224
+ * @param {unknown} value - The value to check for emptiness. It can be of any type.
225
+ * @returns {boolean} `true` if the value is an empty string, an empty object, or a string
226
+ * representation of an empty object; otherwise, `false`.
227
+ */
228
+ function hasEmptyValues(value) {
229
+ if (isString(value)) {
230
+ try {
231
+ if (isEmptyObject(JSON.parse(value))) return true;
232
+ } catch {
233
+ return isEmptyString(value);
234
+ }
235
+ return isEmptyString(value);
236
+ }
237
+ return isEmptyObject(value);
238
+ }
239
+ function parseArray(value, defaultValue) {
240
+ if (isArray(value)) return [...value];
241
+ if (isString(value)) return value.split(/[,|;\n\t ]+/).map((entry) => {
242
+ return entry.trim();
243
+ }).filter((entry) => {
244
+ return entry.length > EMPTY_LENGTH;
245
+ });
246
+ if (isNumber(value)) return [value];
247
+ if (isEmptyObject(value)) return [value];
248
+ if (isNull(value) || isUndefined(value)) return cloneDefaultArray(defaultValue);
249
+ return cloneDefaultArray(defaultValue);
250
+ }
251
+ /**
252
+ * Compares two arrays and returns true if they have at least one common value.
253
+ * @param {readonly T[]} array1 - First array.
254
+ * @param {readonly T[]} array2 - Second array.
255
+ * @returns {boolean} True if the arrays have at least one common value.
256
+ */
257
+ function arrayContainsCommonValue(array1, array2) {
258
+ if (!isArray(array1) || !isArray(array2)) return false;
259
+ const valueOccurrences = new Set(array1);
260
+ return array2.some((value) => {
261
+ return valueOccurrences.has(value);
262
+ });
263
+ }
264
+ /**
265
+ * Returns true if the input is a UUID string.
266
+ * @param {string} input - The input to check.
267
+ * @returns {boolean} True if the input is a UUID string.
268
+ */
269
+ function isUuidString(input) {
270
+ return isString(input) && uuidRegex.test(input);
271
+ }
272
+ /**
273
+ * Returns true if the input is a ULID string.
274
+ * @param {string }input - The input to check.
275
+ * @returns {boolean} True if the input is a ULID string.
276
+ */
277
+ function isUlidString(input) {
278
+ return typeof input === "string" && ulidRegex.test(input);
279
+ }
280
+ function parseDomainName(url, defaultValue) {
281
+ const normalizedValue = url.trim();
282
+ if (normalizedValue === "" || normalizedValue.startsWith("/")) return defaultValue;
283
+ let urlCandidate = normalizedValue;
284
+ if (!normalizedValue.includes("://")) urlCandidate = `https://${normalizedValue}`;
285
+ let hostname = "";
286
+ try {
287
+ ({hostname} = new URL(urlCandidate));
288
+ } catch {
289
+ return defaultValue;
290
+ }
291
+ const [domainName] = hostname.replace(/^www\d?\./i, "").split(".");
292
+ if (!domainName) return defaultValue;
293
+ return domainName;
294
+ }
295
+ //#endregion
296
+ export { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
package/globals.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type {
2
+ JsonifiedObject as JsonifiedObjectImport,
3
+ JsonifiedValue as JsonifiedValueImport,
4
+ Stringified as StringifiedImport,
5
+ } from "./src/types/json.js";
6
+ import type { Maybe as MaybeImport, ResolveMaybe as ResolveMaybeImport } from "./src/types/maybe.js";
7
+ import type {
8
+ Failed as FailedImport,
9
+ MaybePromise as MaybePromiseImport,
10
+ Success as SuccessImport,
11
+ } from "./src/types/maybe_promise.js";
12
+ import type {
13
+ Nullable as NullableImport,
14
+ ResolveNullable as ResolveNullableImport,
15
+ } from "./src/types/nullable.js";
16
+ import type {
17
+ Optional as OptionalImport,
18
+ ResolveOptional as ResolveOptionalImport,
19
+ } from "./src/types/optional.js";
20
+
21
+ declare global {
22
+ type Failed<T extends null = null> = FailedImport<T>;
23
+ type JsonifiedValue<T> = JsonifiedValueImport<T>;
24
+ type Maybe<T> = MaybeImport<T>;
25
+ type MaybePromise<T> = MaybePromiseImport<T>;
26
+ type Nullable<T> = NullableImport<T>;
27
+ type Optional<T> = OptionalImport<T>;
28
+ type ResolveMaybe<T, R extends Maybe<T>> = ResolveMaybeImport<T, R>;
29
+ type ResolveNullable<T, R extends Nullable<T>> = ResolveNullableImport<T, R>;
30
+ type ResolveOptional<T, R extends Optional<T>> = ResolveOptionalImport<T, R>;
31
+ type Stringified<T> = StringifiedImport<T>;
32
+ type Success<T> = SuccessImport<T>;
33
+
34
+ interface JSON {
35
+ stringify<T>(
36
+ value: T,
37
+ replacer?: null,
38
+ space?: string | number,
39
+ ): Stringified<T>;
40
+
41
+ parse<T>(
42
+ str: Stringified<T>,
43
+ replacer?: null,
44
+ ): JsonifiedObjectImport<T>;
45
+ }
46
+ }
47
+
48
+ export type {
49
+ FailedImport as Failed,
50
+ JsonifiedValueImport as JsonifiedValue,
51
+ MaybeImport as Maybe,
52
+ MaybePromiseImport as MaybePromise,
53
+ NullableImport as Nullable,
54
+ OptionalImport as Optional,
55
+ ResolveMaybeImport as ResolveMaybe,
56
+ ResolveNullableImport as ResolveNullable,
57
+ ResolveOptionalImport as ResolveOptional,
58
+ StringifiedImport as Stringified,
59
+ SuccessImport as Success,
60
+ };
package/jsr.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://jsr.io/schema/config-file.v1.json",
3
+ "name": "@murky-web/typebuddy",
4
+ "version": "0.1.0",
5
+ "license": "ISC",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./biome": "./biome/index.ts",
9
+ "./oxlint": "./oxlint/index.ts"
10
+ },
11
+ "publish": {
12
+ "include": [
13
+ "README.md",
14
+ "src/**/*.ts",
15
+ "oxlint/**/*.ts",
16
+ "rules/**/*.ts",
17
+ "biome/**/*.ts",
18
+ "biome/*.grit",
19
+ "biome/*.gritt"
20
+ ],
21
+ "exclude": [
22
+ "tests/**",
23
+ "smoke/**",
24
+ "globals.ts",
25
+ "dist/**",
26
+ "node_modules/**"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,30 @@
1
+ import { requireTryCatchAsyncRule } from "../rules/async_rule.js";
2
+ import { errorSafeAsyncRule } from "../rules/maybe_promise_rule.js";
3
+ import { maybeRule } from "../rules/maybe_rule.js";
4
+ import { nullableRule } from "../rules/nullable_rule.js";
5
+ import { optionalRule } from "../rules/optional_rule.js";
6
+
7
+ const rules = {
8
+ "prefer-maybe": maybeRule,
9
+ "prefer-maybe-promise": errorSafeAsyncRule,
10
+ "prefer-nullable": nullableRule,
11
+ "prefer-optional": optionalRule,
12
+ "require-try-catch": requireTryCatchAsyncRule,
13
+ } as const satisfies Record<string, unknown>;
14
+
15
+ type TypebuddyOxlintPlugin = {
16
+ meta: {
17
+ name: string;
18
+ };
19
+ rules: Record<string, unknown>;
20
+ };
21
+
22
+ const plugin: TypebuddyOxlintPlugin = {
23
+ meta: {
24
+ name: "typebuddy",
25
+ },
26
+ rules,
27
+ };
28
+
29
+ export default plugin;
30
+ export { plugin as typebuddyOxlintPlugin };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@murky-web/typebuddy",
3
+ "version": "0.1.0",
4
+ "description": "Your new best friend for simple typescript guards every project needs.",
5
+ "private": false,
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./globals": {
16
+ "types": "./globals.ts",
17
+ "import": "./dist/globals.js",
18
+ "default": "./dist/globals.js"
19
+ },
20
+ "./oxlint": {
21
+ "types": "./oxlint/index.ts",
22
+ "import": "./dist/oxlint.js",
23
+ "default": "./dist/oxlint.js"
24
+ },
25
+ "./biome": {
26
+ "types": "./biome/index.ts",
27
+ "import": "./dist/biome.js",
28
+ "default": "./dist/biome.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "scripts": {
33
+ "test": "vitest run",
34
+ "build:local": "bun run build",
35
+ "build": "bun run clean && tsdown && cp ./biome/*.grit ./dist/ && cp ./biome/*.gritt ./dist/",
36
+ "clean": "rm -rf dist",
37
+ "lint": "oxlint -c ./.oxlintrc.jsonc --tsconfig ./tsconfig.lint.json --type-aware src biome tsdown.config.ts vitest.config.ts",
38
+ "lint:fix": "oxlint -c ./.oxlintrc.jsonc --tsconfig ./tsconfig.lint.json --type-aware --fix src biome tsdown.config.ts vitest.config.ts",
39
+ "format": "oxfmt -c ../../node_modules/@murky-web/config/oxc/.oxfmtrc.jsonc src rules biome tests tsdown.config.ts vitest.config.ts",
40
+ "format:check": "oxfmt -c ../../node_modules/@murky-web/config/oxc/.oxfmtrc.jsonc --check src rules biome tests tsdown.config.ts vitest.config.ts",
41
+ "smoke:globals": "bun run build && tsgo --project ./smoke/globals/tsconfig.json --noEmit",
42
+ "smoke:oxlint": "bun run build && bun ./smoke/oxlint/run-smoke.ts",
43
+ "smoke:oxlint:fix": "bun run build && bun ./smoke/oxlint/run-fix-smoke.ts",
44
+ "smoke:treeshake": "bun run build && bun ./smoke/treeshake/run.ts",
45
+ "typecheck": "tsgo --project ./tsconfig.json --noEmit && tsgo --project ./tsconfig.vitest.json --noEmit"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "globals.ts",
50
+ "jsr.json",
51
+ "src/index.ts",
52
+ "src/type_helper.ts",
53
+ "src/types",
54
+ "oxlint/index.ts",
55
+ "biome",
56
+ "rules"
57
+ ],
58
+ "license": "ISC",
59
+ "type": "module",
60
+ "sideEffects": false,
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "provenance":false
64
+ },
65
+ "devDependencies": {
66
+ "@murky-web/config": "workspace:*"
67
+ }
68
+ }
@@ -0,0 +1,98 @@
1
+ type AstNode = {
2
+ type: string;
3
+ body?: AstNode | AstNode[];
4
+ async?: boolean;
5
+ range?: [number, number];
6
+ [key: string]: unknown;
7
+ };
8
+
9
+ type RuleContext = {
10
+ getSourceCode(): { getText(node: unknown): string };
11
+ report(descriptor: {
12
+ node: unknown;
13
+ messageId: string;
14
+ fix(
15
+ fixer: {
16
+ replaceText(node: unknown, text: string): unknown;
17
+ },
18
+ ): unknown;
19
+ }): void;
20
+ };
21
+
22
+ function hasTryCatch(nodes: AstNode[]): boolean {
23
+ return nodes.some((node) => {return node.type === "TryStatement"});
24
+ }
25
+
26
+ const rule = {
27
+ create(context: RuleContext) {
28
+ const sourceCode = context.getSourceCode();
29
+
30
+ function getIndentation(node: AstNode): string {
31
+ const lines = sourceCode.getText(node).split("\n");
32
+ const firstLine = lines[0];
33
+ const match = /^\s*/.exec(firstLine);
34
+ return match ? match[0] : "";
35
+ }
36
+
37
+ function wrapInTryCatch(node: AstNode): string {
38
+ const body = Array.isArray(node.body) ? node.body : [];
39
+ const indent = getIndentation(node);
40
+ const innerIndent = `${indent} `;
41
+ const bodyText = body
42
+ .map((statement) => {
43
+ const text = sourceCode.getText(statement);
44
+ return `${innerIndent}${text.replaceAll(/^\s*/gm, "")}`;
45
+ })
46
+ .join("\n");
47
+
48
+ return `{
49
+ ${indent}try {
50
+ ${bodyText}
51
+ ${indent}} catch (err) {
52
+ ${innerIndent}return FAILED_PROMISE;
53
+ ${indent}}
54
+ }`;
55
+ }
56
+
57
+ function checkFunction(node: AstNode) {
58
+ if (node.async !== true) return;
59
+ const bodyNode = node.body;
60
+ if (!bodyNode || Array.isArray(bodyNode) || bodyNode.type !== "BlockStatement") {
61
+ return;
62
+ }
63
+
64
+ const body = Array.isArray(bodyNode.body) ? bodyNode.body : [];
65
+ if (hasTryCatch(body)) return;
66
+
67
+ context.report({
68
+ node,
69
+ messageId: "missingTryCatch",
70
+ fix(fixer) {
71
+ return fixer.replaceText(bodyNode, wrapInTryCatch(bodyNode));
72
+ },
73
+ });
74
+ }
75
+
76
+ return {
77
+ FunctionDeclaration: checkFunction,
78
+ FunctionExpression: checkFunction,
79
+ ArrowFunctionExpression: checkFunction,
80
+ };
81
+ },
82
+ defaultOptions: [],
83
+ meta: {
84
+ type: "suggestion",
85
+ docs: {
86
+ description:
87
+ "Async functions should have a try-catch block returning FAILED_PROMISE.",
88
+ },
89
+ fixable: "code",
90
+ schema: [],
91
+ messages: {
92
+ missingTryCatch:
93
+ "Async functions must have a try-catch block returning FAILED_PROMISE.",
94
+ },
95
+ },
96
+ };
97
+
98
+ export { rule as requireTryCatchAsyncRule };