Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/dry-ends-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@layerstack/svelte-actions': patch
'@layerstack/svelte-stores': patch
'@layerstack/svelte-state': patch
'@layerstack/svelte-table': patch
'@layerstack/tailwind': patch
'@layerstack/utils': patch
---

Remove lodash-es dependency
1 change: 0 additions & 1 deletion packages/svelte-stores/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"dependencies": {
"@layerstack/utils": "workspace:*",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"zod": "^3.24.3"
},
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-stores/src/lib/fetchStore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getContext, setContext } from 'svelte';
import { get, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import { merge } from 'lodash-es';
import { merge } from '@layerstack/utils';

type BodyMethods = 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text';

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-stores/src/lib/formStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type Patch,
} from 'immer';
import type { Schema } from 'zod';
import { set } from 'lodash-es';
import { set } from '@layerstack/utils';

// Needed for finishDraft() patches/inverseChanges - https://immerjs.github.io/immer/patches
enablePatches();
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-stores/src/lib/graphStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { setContext, getContext } from 'svelte';
import { writable } from 'svelte/store';
import { merge } from 'lodash-es';
import { merge } from '@layerstack/utils';

import { parse, stringify } from '@layerstack/utils';

Expand Down
3 changes: 1 addition & 2 deletions packages/svelte-stores/src/lib/queryParamsStore.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { derived, get, type Readable } from 'svelte/store';
import type { Page } from '@sveltejs/kit';
import { isEqual } from 'lodash-es';

import * as Serialize from '@layerstack/utils/serialize';
import rollup from '@layerstack/utils/rollup';
import { entries, type ValueOf } from '@layerstack/utils';
import { entries, isEqual, type ValueOf } from '@layerstack/utils';

// Matches $app/navigation's goto without dependency - https://kit.svelte.dev/docs/modules#$app-navigation-goto
type Goto = (url: string | URL, opts?: any) => any;
Expand Down
3 changes: 1 addition & 2 deletions packages/svelte-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
"dependencies": {
"@layerstack/svelte-actions": "workspace:*",
"@layerstack/utils": "workspace:*",
"d3-array": "^3.2.4",
"lodash-es": "^4.17.21"
"d3-array": "^3.2.4"
},
"main": "./dist/index.js",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-table/src/lib/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Action } from 'svelte/action';
import { merge } from 'lodash-es';
import { merge } from '@layerstack/utils';
import { extent, max, min } from 'd3-array';

import type { tableOrderStore } from './stores.js';
Expand Down
4 changes: 1 addition & 3 deletions packages/svelte-table/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { get } from 'lodash-es';

import { PeriodType, parseDate } from '@layerstack/utils';
import { PeriodType, parseDate, get } from '@layerstack/utils';

import type { ColumnDef } from './types.js';

Expand Down
1 change: 0 additions & 1 deletion packages/tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@layerstack/utils": "workspace:^",
"clsx": "^2.1.1",
"d3-array": "^3.2.4",
"lodash-es": "^4.17.21",
"tailwind-merge": "^3.2.0"
},
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwind/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import clsx, { type ClassValue } from 'clsx';
import { extendTailwindMerge } from 'tailwind-merge';
import { range } from 'd3-array';
import { mergeWith } from 'lodash-es';
import { mergeWith } from '@layerstack/utils';

/**
* Wrapper around `tailwind-merge` and `clsx`
Expand Down
3 changes: 1 addition & 2 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
"dependencies": {
"d3-array": "^3.2.4",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"lodash-es": "^4.17.21"
"d3-time-format": "^4.1.0"
},
"main": "./dist/index.js",
"exports": {
Expand Down
82 changes: 82 additions & 0 deletions packages/utils/src/lib/get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect } from 'vitest';

import { get } from './get.js';

describe('get', () => {
it('returns value at string path', () => {
const obj = { a: { b: { c: 3 } } };
expect(get(obj, 'a.b.c')).toBe(3);
});

it('returns value at array path', () => {
const obj = { a: { b: { c: 3 } } };
expect(get(obj, ['a', 'b', 'c'])).toBe(3);
});

it('returns value at symbol path', () => {
const sym = Symbol('key');
const obj = { [sym]: 'value' };
expect(get(obj, sym)).toBe('value');
});

it('returns defaultValue when path does not exist', () => {
const obj = { a: { b: 1 } };
expect(get(obj, 'a.c', 'default')).toBe('default');
expect(get(obj, 'x.y.z', 42)).toBe(42);
});

it('returns defaultValue for null/undefined object', () => {
expect(get(null, 'a.b', 'default')).toBe('default');
expect(get(undefined, 'a.b', 'default')).toBe('default');
});

it('returns undefined when path does not exist and no default provided', () => {
const obj = { a: 1 };
expect(get(obj, 'b')).toBeUndefined();
expect(get(obj, 'a.b.c')).toBeUndefined();
});

it('handles array indices in string path', () => {
const obj = { a: [{ b: 1 }, { b: 2 }] };
expect(get(obj, 'a.0.b')).toBe(1);
expect(get(obj, 'a.1.b')).toBe(2);
});

it('handles array indices in array path', () => {
const obj = { a: [{ b: 1 }, { b: 2 }] };
expect(get(obj, ['a', 0, 'b'])).toBe(1);
expect(get(obj, ['a', 1, 'b'])).toBe(2);
});

it('returns value when path leads to falsy value', () => {
const obj = { a: { b: 0, c: false, d: '', e: null } };
expect(get(obj, 'a.b', 'default')).toBe(0);
expect(get(obj, 'a.c', 'default')).toBe(false);
expect(get(obj, 'a.d', 'default')).toBe('');
expect(get(obj, 'a.e', 'default')).toBe(null);
});

it('returns defaultValue when intermediate path is undefined', () => {
const obj = { a: undefined };
expect(get(obj, 'a.b', 'default')).toBe('default');
});

it('does not mutate the original object', () => {
const obj = { a: { b: 1 } };
const original = JSON.stringify(obj);
get(obj, 'a.b');
get(obj, 'a.c', 'default');
expect(JSON.stringify(obj)).toBe(original);
});

it('handles empty string path', () => {
const obj = { '': 'empty-key-value' };
expect(get(obj, '')).toBe('empty-key-value');
});

it('handles deeply nested paths', () => {
const obj = { a: { b: { c: { d: { e: { f: 'deep' } } } } } };
expect(get(obj, 'a.b.c.d.e.f')).toBe('deep');
expect(get(obj, ['a', 'b', 'c', 'd', 'e', 'f'])).toBe('deep');
});
});
39 changes: 39 additions & 0 deletions packages/utils/src/lib/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* See: https://github.com/angus-c/just/blob/d8c5dd18941062d8db7e9310ecc8f53fd607df54/packages/object-safe-get/index.mjs#L33C1-L61C2
*/
export function get<T = any>(
obj: any,
propsArg: string | symbol | (string | number | symbol)[],
defaultValue?: T
): T {
if (!obj) {
return defaultValue as T;
}

let props: (string | number | symbol)[];

if (Array.isArray(propsArg)) {
props = propsArg.slice(0);
} else if (typeof propsArg === 'string') {
props = propsArg.split('.');
} else if (typeof propsArg === 'symbol') {
props = [propsArg];
} else {
throw new Error('props arg must be an array, a string or a symbol');
}

let result: any = obj;

while (props.length) {
const prop = props.shift()!;
if (!result) {
return defaultValue as T;
}
result = result[prop];
if (result === undefined) {
return defaultValue as T;
}
}

return result;
}
4 changes: 4 additions & 0 deletions packages/utils/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export {
export * from './json.js';
export * from './logger.js';
export { round, clamp, randomInteger } from './number.js';
export { get } from './get.js';
export { set } from './set.js';
export { isEqual } from './isEqual.js';
export { isEmptyObject, isLiteralObject, omit, pick } from './object.js';
export { mergeWith, merge, defaultsDeep } from './mergeWith.js';
export * from './promise.js';
export * from './sort.js';
export * from './string.js';
Expand Down
46 changes: 46 additions & 0 deletions packages/utils/src/lib/isEqual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';

import { isEqual } from './isEqual.js';

describe('isEqual', () => {
it('compares primitives', () => {
expect(isEqual(1, 1)).toBe(true);
expect(isEqual(1, 2)).toBe(false);
expect(isEqual('a', 'a')).toBe(true);
expect(isEqual(true, false)).toBe(false);
expect(isEqual(null, null)).toBe(true);
expect(isEqual(undefined, undefined)).toBe(true);
expect(isEqual(null, undefined)).toBe(false);
});

it('treats NaN as equal to NaN', () => {
expect(isEqual(Number.NaN, Number.NaN)).toBe(true);
});

it('compares arrays deeply', () => {
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false);
});

it('compares objects deeply', () => {
expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true);
expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } })).toBe(false);
});

it('compares dates by timestamp', () => {
expect(
isEqual(new Date('2020-01-01T00:00:00.000Z'), new Date('2020-01-01T00:00:00.000Z'))
).toBe(true);
expect(
isEqual(new Date('2020-01-01T00:00:00.000Z'), new Date('2020-01-02T00:00:00.000Z'))
).toBe(false);
});

it('handles circular references', () => {
const a: any = { x: 1 };
a.self = a;
const b: any = { x: 1 };
b.self = b;
expect(isEqual(a, b)).toBe(true);
});
});
40 changes: 40 additions & 0 deletions packages/utils/src/lib/isEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export function isEqual(a: any, b: any, seen = new WeakMap()): boolean {
if (a === b || (a !== a && b !== b)) return true; // identical or both NaN
if (a == null || b == null) return a === b;
if (typeof a !== 'object' || typeof b !== 'object') return false;

// Circular reference handling
if (seen.has(a)) return seen.get(a) === b;
seen.set(a, b);

// Dates
if (a instanceof Date && b instanceof Date) return +a === +b;
if (a instanceof Date || b instanceof Date) return false;

// Maps
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [k, v] of a) if (!b.has(k) || !isEqual(v, b.get(k), seen)) return false;
return true;
}
if (a instanceof Map || b instanceof Map) return false;

// Sets
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
for (const v of a) if (!b.has(v)) return false;
return true;
}
if (a instanceof Set || b instanceof Set) return false;

// Arrays
if (Array.isArray(a) && Array.isArray(b)) {
return a.length === b.length && a.every((v, i) => isEqual(v, b[i], seen));
}
if (Array.isArray(a) || Array.isArray(b)) return false;

// Plain objects
const keysA = Object.keys(a);
const keysB = Object.keys(b);
return keysA.length === keysB.length && keysA.every((k) => k in b && isEqual(a[k], b[k], seen));
}
2 changes: 1 addition & 1 deletion packages/utils/src/lib/locale.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { entries, fromEntries, type Prettify } from './typeHelpers.js';
import { defaultsDeep } from 'lodash-es';
import { defaultsDeep } from './mergeWith.js';
import { derived, writable, type Readable, type Writable } from 'svelte/store';
import {
DateToken,
Expand Down
Loading
Loading