Skip to content

Commit

Permalink
add box utilities (#15)
Browse files Browse the repository at this point in the history
* add box

* add readonlyBox

* tests and use box internally

* test value or box

* prettier is dog-shit

* update box

* format

* fix exports

* update box once more

* fix other examples

* fix prettier

* flatten and docs

* format

* fix lint

* cleanup lint

* not needed

* update ci

* PR changes

---------

Co-authored-by: Hunter Johnston <[email protected]>
Co-authored-by: AdrianGonz97 <[email protected]>
  • Loading branch information
3 people authored Apr 25, 2024
1 parent bdffcb7 commit 9787d92
Show file tree
Hide file tree
Showing 22 changed files with 1,269 additions and 368 deletions.
5 changes: 5 additions & 0 deletions .changeset/spicy-nails-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

add `box` utilities
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,43 @@ jobs:
# - name: Run svelte-check
# run: pnpm check

Test:
runs-on: ubuntu-latest
name: Test
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Install Node.JS
uses: actions/setup-node@v3
with:
node-version: 18

- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8

# PNPM Store cache setup
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install

- run: pnpm test

Lint:
runs-on: ubuntu-latest
name: Lint
Expand Down
3 changes: 1 addition & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,

// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},

"editor.formatOnSave": true,
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
Expand Down
14 changes: 13 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import config, { DEFAULT_IGNORES } from "@huntabyte/eslint-config";

const CUSTOM_IGNORES = ["**/.github/**", "CHANGELOG.md", "**/.contentlayer"];
const CUSTOM_IGNORES = [
"**/.github/**",
"CHANGELOG.md",
"**/.contentlayer",
"**/node_modules/**",
"**/.svelte-kit/**",
".svelte-kit/**/*",
"*.md",
];

export default config({
svelte: true,
ignores: [...DEFAULT_IGNORES, ...CUSTOM_IGNORES],
}).override("antfu/typescript/rules", {
rules: {
"ts/consistent-type-definitions": "off",
},
});
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
"url": "https://github.com/svecosystem/runed"
},
"scripts": {
"test": "pnpm -r test",
"dev": "pnpm sync && pnpm --parallel dev",
"test": "pnpm -r test",
"test:package": "pnpm -F \"./packages/**\" test",
"test:package:watch": "pnpm -F \"./packages/**\" test:watch",
"build": "pnpm -r build",
"build:packages": "pnpm -F \"./packages/**\" --parallel build",
"build:content": "pnpm -F \"./sites/**\" --parallel build:content",
Expand All @@ -39,14 +41,14 @@
"license": "MIT",
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@huntabyte/eslint-config": "^0.2.0",
"@huntabyte/eslint-config": "^0.3.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"eslint": "^8.56.0",
"eslint-plugin-svelte": "2.36.0-next.13",
"eslint": "^9.1.1",
"eslint-plugin-svelte": "2.38.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.5.13",
"svelte-eslint-parser": "^0.33.1"
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte-eslint-parser": "^0.35.0"
},
"type": "module"
}
6 changes: 4 additions & 2 deletions packages/runed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"dev": "pnpm sync && pnpm watch",
"build": "pnpm package",
"package": "svelte-kit sync && svelte-package && publint",
"test": "vitest",
"test": "vitest --run",
"test:watch": "vitest --watch",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"watch": "svelte-kit sync && svelte-package --watch"
},
Expand All @@ -48,14 +49,15 @@
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/node": "^20.10.6",
"@vitest/coverage-v8": "^1.5.1",
"jsdom": "^24.0.0",
"publint": "^0.1.9",
"svelte": "^5.0.0-next.110",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"vitest": "^1.0.0"
"vitest": "^1.5.1"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
13 changes: 0 additions & 13 deletions packages/runed/postcss.config.cjs

This file was deleted.

192 changes: 192 additions & 0 deletions packages/runed/src/lib/functions/box/box.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type { Expand, Getter } from "$lib/internal/types.js";
import { isFunction, isObject } from "$lib/internal/utils/is.js";

const BoxSymbol = Symbol("box");
const isWritableSymbol = Symbol("is-writable");

export interface ReadableBox<T> {
readonly [BoxSymbol]: true;
readonly value: T;
}

export interface WritableBox<T> extends ReadableBox<T> {
readonly [isWritableSymbol]: true;
value: T;
}

/**
* @returns Whether the value is a Box
*/
function isBox(value: unknown): value is ReadableBox<unknown> {
return isObject(value) && BoxSymbol in value;
}
/**
* @returns Whether the value is a WritableBox
*/
function isWritableBox(value: unknown): value is WritableBox<unknown> {
return box.isBox(value) && isWritableSymbol in value;
}

/**
* Creates a writable box.
*
* @returns A box with a `value` property which can be set to a new value.
* Useful to pass state to other functions.
*/
export function box<T>(): WritableBox<T | undefined>;
/**
* Creates a writable box with an initial value.
*
* @param initialValue The initial value of the box.
* @returns A box with a `value` property which can be set to a new value.
* Useful to pass state to other functions.
*/
export function box<T>(initialValue: T): WritableBox<T>;
export function box(initialValue?: unknown) {
let value = $state(initialValue);

return {
[BoxSymbol]: true,
[isWritableSymbol]: true,
get value() {
return value as unknown;
},
set value(v: unknown) {
value = v;
},
};
}

/**
* Creates a readonly box
*
* @param getter Function to get the value of the box
* @returns A box with a `value` property whose value is the result of the getter.
* Useful to pass state to other functions.
*/
function boxWith<T>(getter: () => T): ReadableBox<T>;
/**
* Creates a writable box
*
* @param getter Function to get the value of the box
* @param setter Function to set the value of the box
* @returns A box with a `value` property which can be set to a new value.
* Useful to pass state to other functions.
*/
function boxWith<T>(getter: () => T, setter: (v: T) => void): WritableBox<T>;
function boxWith<T>(getter: () => T, setter?: (v: T) => void) {
const derived = $derived.by(getter);

if (setter) {
return {
[BoxSymbol]: true,
[isWritableSymbol]: true,
get value() {
return derived;
},
set value(v: T) {
setter(v);
},
};
}

return {
[BoxSymbol]: true,
get value() {
return getter();
},
};
}

export type BoxFrom<T> =
T extends WritableBox<infer U>
? WritableBox<U>
: T extends ReadableBox<infer U>
? ReadableBox<U>
: T extends Getter<infer U>
? ReadableBox<U>
: WritableBox<T>;

/**
* Creates a box from either a static value, a box, or a getter function.
* Useful when you want to receive any of these types of values and generate a boxed version of it.
*
* @returns A box with a `value` property whose value.
*/
function boxFrom<T>(value: T): BoxFrom<T> {
if (box.isBox(value)) return value as BoxFrom<T>;
if (isFunction(value)) return box.with(value) as BoxFrom<T>;
return box(value) as BoxFrom<T>;
}

type GetKeys<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
type RemoveValues<T, U> = Omit<T, GetKeys<T, U>>;

type BoxFlatten<R extends Record<string, unknown>> = Expand<
RemoveValues<
{
[K in keyof R]: R[K] extends WritableBox<infer T> ? T : never;
},
never
> &
RemoveValues<
{
readonly [K in keyof R]: R[K] extends WritableBox<infer _>
? never
: R[K] extends ReadableBox<infer T>
? T
: never;
},
never
>
> &
RemoveValues<
{
[K in keyof R]: R[K] extends ReadableBox<infer _> ? never : R[K];
},
never
>;

/**
* Function that gets an object of boxes, and returns an object of reactive values
*
* @example
* const count = box(0)
* const flat = box.flatten({ count, double: box.with(() => count.value) })
* // type of flat is { count: number, readonly double: number }
*/
function boxFlatten<R extends Record<string, unknown>>(boxes: R): BoxFlatten<R> {
return Object.entries(boxes).reduce<BoxFlatten<R>>((acc, [key, b]) => {
if (!box.isBox(b)) {
return Object.assign(acc, { [key]: b });
}

if (box.isWritableBox(b)) {
Object.defineProperty(acc, key, {
get() {
return b.value;
},
// eslint-disable-next-line ts/no-explicit-any
set(v: any) {
b.value = v;
},
});
} else {
Object.defineProperty(acc, key, {
get() {
return b.value;
},
});
}

return acc;
}, {} as BoxFlatten<R>);
}

box.from = boxFrom;
box.with = boxWith;
box.flatten = boxFlatten;
box.isBox = isBox;
box.isWritableBox = isWritableBox;
Loading

0 comments on commit 9787d92

Please sign in to comment.