Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add box utilities #15

Merged
merged 18 commits into from
Apr 25, 2024
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
Loading