mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -05:00
WIP: adding an @astrojs/store package and useStore for framework integrations
This commit is contained in:
parent
9b530bdece
commit
ac4b16d7ec
17 changed files with 346 additions and 0 deletions
36
packages/astro-store/package.json
Normal file
36
packages/astro-store/package.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "@astrojs/store",
|
||||
"description": "Add framework-agnostic stores to your Astro projects",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"author": "withastro",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/withastro/astro.git",
|
||||
"directory": "packages/astro-store"
|
||||
},
|
||||
"bugs": "https://github.com/withastro/astro/issues",
|
||||
"homepage": "https://astro.build",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"test": "mocha --exit --timeout 20000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^9.2.2"
|
||||
}
|
||||
}
|
14
packages/astro-store/src/context.ts
Normal file
14
packages/astro-store/src/context.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
const contexts = new Map();
|
||||
|
||||
export function getContext<T>(key: any): T {
|
||||
return contexts.get(key);
|
||||
}
|
||||
|
||||
export function setContext<T>(key: any, value: T): T {
|
||||
contexts.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function hasContext(key: any): boolean {
|
||||
return contexts.has(key);
|
||||
}
|
2
packages/astro-store/src/index.ts
Normal file
2
packages/astro-store/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './context';
|
||||
export * from './store';
|
158
packages/astro-store/src/store.ts
Normal file
158
packages/astro-store/src/store.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import { getStoreValue, isFunction, noop, runAll, safeNotEqual, subscribe } from './utils';
|
||||
|
||||
export type Subscriber<T> = (value: T) => void;
|
||||
export type Unsubscriber = () => void;
|
||||
export type Updater<T> = (value: T) => T;
|
||||
export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
|
||||
|
||||
type Invalidator<T> = (value?: T) => void;
|
||||
|
||||
export interface Readable<T> {
|
||||
subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber;
|
||||
}
|
||||
|
||||
export interface Writable<T> extends Readable<T> {
|
||||
set(this: void, value: T): void;
|
||||
update(this: void, updater: Updater<T>): void;
|
||||
}
|
||||
|
||||
type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidator<T>];
|
||||
|
||||
const subscriberQueue: any[] = [];
|
||||
|
||||
export function readable<T>(value?: T, start?: StartStopNotifier<T>): Readable<T> {
|
||||
return {
|
||||
subscribe: writable(value, start).subscribe
|
||||
}
|
||||
}
|
||||
|
||||
export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writable<T> {
|
||||
let stop: Unsubscriber | null = null;
|
||||
const subscribers: Set<SubscribeInvalidateTuple<T>> = new Set();
|
||||
|
||||
function set(newValue: T): void {
|
||||
if (safeNotEqual(value, newValue)) {
|
||||
value = newValue;
|
||||
if (stop !== null) { // store is ready
|
||||
const runQueue = !subscriberQueue.length;
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber[1]();
|
||||
subscriberQueue.push(subscriber, value);
|
||||
}
|
||||
if (runQueue) {
|
||||
for (let i = 0; i < subscriberQueue.length; i += 2) {
|
||||
subscriberQueue[i][0](subscriberQueue[i + 1]);
|
||||
}
|
||||
subscriberQueue.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(fn: Updater<T>): void {
|
||||
set(fn(value!));
|
||||
}
|
||||
|
||||
function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber {
|
||||
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
|
||||
subscribers.add(subscriber);
|
||||
if (subscribers.size === 1) {
|
||||
stop = start(set) || noop;
|
||||
}
|
||||
|
||||
run(value!);
|
||||
|
||||
return () => {
|
||||
subscribers.delete(subscriber);
|
||||
if (subscribers.size === 0) {
|
||||
stop!();
|
||||
stop = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set,
|
||||
update,
|
||||
subscribe
|
||||
};
|
||||
}
|
||||
|
||||
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>] | Array<Readable<any>>;
|
||||
|
||||
type StoresValues<T> = T extends Readable<infer U> ? U :
|
||||
{ [K in keyof T]: T[K] extends Readable<infer U> ? U : never };
|
||||
|
||||
export function derived<S extends Stores, T>(
|
||||
stores: S,
|
||||
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
|
||||
initialValue?: T
|
||||
): Readable<T>;
|
||||
|
||||
export function derived<S extends Stores, T>(
|
||||
stores: S,
|
||||
fn: (values: StoresValues<S>) => T,
|
||||
initialValue?: T
|
||||
): Readable<T>;
|
||||
|
||||
export function derived<S extends Stores, T>(
|
||||
stores: S,
|
||||
fn: (values: StoresValues<S>) => T
|
||||
): Readable<T>;
|
||||
|
||||
export function derived<T>(stores: Stores, fn: Function, initialValue?: T): Readable<T> {
|
||||
const isSingle = !Array.isArray(stores);
|
||||
const storesArray: Array<Readable<any>> = isSingle
|
||||
? [stores as Readable<any>]
|
||||
: stores as Array<Readable<any>>;
|
||||
|
||||
const auto = fn.length < 2;
|
||||
|
||||
return readable(initialValue, (set) => {
|
||||
let initialized = false;
|
||||
const values: any[] = [];
|
||||
|
||||
let pending = 0;
|
||||
let cleanup = noop;
|
||||
|
||||
const sync = () => {
|
||||
if (pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
const result = fn(isSingle ? values[0] : values, set);
|
||||
|
||||
if (auto) {
|
||||
set(result as T);
|
||||
} else {
|
||||
cleanup = isFunction(result) ? result as Unsubscriber : noop;
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribers = storesArray.map((store, i) => subscribe(
|
||||
store,
|
||||
(value: any) => {
|
||||
values[i] = value;
|
||||
pending &= ~(1 << i);
|
||||
if (initialized) {
|
||||
sync();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
pending |= (1 << i);
|
||||
}
|
||||
));
|
||||
|
||||
initialized = true;
|
||||
sync();
|
||||
|
||||
return function stop() {
|
||||
runAll(unsubscribers);
|
||||
cleanup();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const get = getStoreValue;
|
36
packages/astro-store/src/utils.ts
Normal file
36
packages/astro-store/src/utils.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { Readable } from "./store";
|
||||
|
||||
export function getStoreValue<T>(store: Readable<T>): T {
|
||||
let value: T | undefined = undefined;
|
||||
subscribe(store, (t: T) => value = t)();
|
||||
return value!;
|
||||
}
|
||||
|
||||
export function isFunction(thing: any): thing is Function {
|
||||
return typeof thing === 'function';
|
||||
}
|
||||
|
||||
export function noop() {}
|
||||
|
||||
export function run(fn: Function) {
|
||||
fn();
|
||||
}
|
||||
|
||||
export function runAll(fns: Function[]) {
|
||||
fns.forEach(run);
|
||||
}
|
||||
|
||||
export function safeNotEqual(a: any, b: any) {
|
||||
return a != a
|
||||
? b == b
|
||||
: a !== b || ((a && typeof a === 'object') || typeof a === 'function');
|
||||
}
|
||||
|
||||
export function subscribe<T>(store: Readable<T>, ...callbacks: any[]) {
|
||||
if (store == null) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const result = (store.subscribe as any)(...callbacks);
|
||||
return result.unsubscribe ? () => result.unsubscribe() : result;
|
||||
}
|
10
packages/astro-store/tsconfig.json
Normal file
10
packages/astro-store/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"module": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"target": "ES2020"
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@
|
|||
"preact-render-to-string": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/store": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"preact": "^10.7.3"
|
||||
|
|
13
packages/integrations/preact/src/store.ts
Normal file
13
packages/integrations/preact/src/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { get } from '@astrojs/store';
|
||||
import type { Readable } from '@astrojs/store';
|
||||
|
||||
export function useStore<T>(readable: Readable<T>) {
|
||||
const [state, setState] = useState(get(readable));
|
||||
|
||||
useEffect(() => {
|
||||
return readable.subscribe(setState);
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
|
@ -38,6 +38,7 @@
|
|||
"devDependencies": {
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-dom": "^17.0.17",
|
||||
"@astrojs/store": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"react": "^18.1.0",
|
||||
|
|
13
packages/integrations/react/src/store.ts
Normal file
13
packages/integrations/react/src/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { get } from '@astrojs/store';
|
||||
import type { Readable } from '@astrojs/store';
|
||||
|
||||
export function useStore<T>(readable: Readable<T>) {
|
||||
const [state, setState] = useState(get(readable));
|
||||
|
||||
useEffect(() => {
|
||||
return readable.subscribe(setState);
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
|
@ -34,6 +34,7 @@
|
|||
"babel-preset-solid": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/store": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"solid-js": "^1.4.3"
|
||||
|
|
13
packages/integrations/solid/src/store.ts
Normal file
13
packages/integrations/solid/src/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createEffect, createSignal, onCleanup } from 'solid-js';
|
||||
import { get } from '@astrojs/store';
|
||||
import type { Readable } from '@astrojs/store';
|
||||
|
||||
export function useStore<T>(readable: Readable<T>) {
|
||||
const [state, setState] = createSignal(get(readable));
|
||||
|
||||
createEffect(() => {
|
||||
onCleanup(readable.subscribe(setState as (value: T) => void));
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
|
@ -37,6 +37,7 @@
|
|||
"vite": "^2.9.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/store": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"svelte": "^3.48.0"
|
||||
|
|
5
packages/integrations/svelte/src/store.ts
Normal file
5
packages/integrations/svelte/src/store.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { Readable } from '@astrojs/store';
|
||||
|
||||
export function useStore<T>(readable: Readable<T>) {
|
||||
return readable;
|
||||
}
|
|
@ -35,6 +35,7 @@
|
|||
"vite": "^2.9.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/store": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"vue": "^3.2.36"
|
||||
|
|
12
packages/integrations/vue/src/store.ts
Normal file
12
packages/integrations/vue/src/store.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { getCurrentScope, onScopeDispose, readonly, shallowRef } from 'vue';
|
||||
import { get } from '@astrojs/store';
|
||||
import type { Readable } from '@astrojs/store';
|
||||
|
||||
export function useStore<T>(readable: Readable<T>) {
|
||||
const state = shallowRef(get(readable));
|
||||
|
||||
const unsubscribe = readable.subscribe(value => state.value = value);
|
||||
getCurrentScope() && onScopeDispose(unsubscribe);
|
||||
|
||||
return readonly(state);
|
||||
}
|
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
|
@ -659,6 +659,26 @@ importers:
|
|||
chai-as-promised: 7.1.1_chai@4.3.6
|
||||
mocha: 9.2.2
|
||||
|
||||
packages/astro-store:
|
||||
specifiers:
|
||||
'@types/chai': ^4.3.1
|
||||
'@types/chai-as-promised': ^7.1.5
|
||||
'@types/mocha': ^9.1.1
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
chai: ^4.3.6
|
||||
chai-as-promised: ^7.1.1
|
||||
mocha: ^9.2.2
|
||||
devDependencies:
|
||||
'@types/chai': 4.3.1
|
||||
'@types/chai-as-promised': 7.1.5
|
||||
'@types/mocha': 9.1.1
|
||||
astro: link:../astro
|
||||
astro-scripts: link:../../scripts
|
||||
chai: 4.3.6
|
||||
chai-as-promised: 7.1.1_chai@4.3.6
|
||||
mocha: 9.2.2
|
||||
|
||||
packages/astro/e2e/fixtures/astro-component:
|
||||
specifiers:
|
||||
astro: workspace:*
|
||||
|
@ -1788,6 +1808,7 @@ importers:
|
|||
|
||||
packages/integrations/preact:
|
||||
specifiers:
|
||||
'@astrojs/store': workspace:*
|
||||
'@babel/plugin-transform-react-jsx': ^7.17.12
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
|
@ -1797,12 +1818,14 @@ importers:
|
|||
'@babel/plugin-transform-react-jsx': 7.17.12
|
||||
preact-render-to-string: 5.2.0_preact@10.7.3
|
||||
devDependencies:
|
||||
'@astrojs/store': link:../../astro-store
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
preact: 10.7.3
|
||||
|
||||
packages/integrations/react:
|
||||
specifiers:
|
||||
'@astrojs/store': workspace:*
|
||||
'@babel/plugin-transform-react-jsx': ^7.17.12
|
||||
'@types/react': ^17.0.45
|
||||
'@types/react-dom': ^17.0.17
|
||||
|
@ -1813,6 +1836,7 @@ importers:
|
|||
dependencies:
|
||||
'@babel/plugin-transform-react-jsx': 7.17.12
|
||||
devDependencies:
|
||||
'@astrojs/store': link:../../astro-store
|
||||
'@types/react': 17.0.45
|
||||
'@types/react-dom': 17.0.17
|
||||
astro: link:../../astro
|
||||
|
@ -1835,6 +1859,7 @@ importers:
|
|||
|
||||
packages/integrations/solid:
|
||||
specifiers:
|
||||
'@astrojs/store': workspace:*
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
babel-preset-solid: ^1.4.2
|
||||
|
@ -1842,6 +1867,7 @@ importers:
|
|||
dependencies:
|
||||
babel-preset-solid: 1.4.2
|
||||
devDependencies:
|
||||
'@astrojs/store': link:../../astro-store
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
solid-js: 1.4.3
|
||||
|
@ -1861,6 +1887,7 @@ importers:
|
|||
svelte-preprocess: 4.10.7_xxnnhi7j46bwl35r5gwl6d4d6q
|
||||
vite: 2.9.10
|
||||
devDependencies:
|
||||
'@astrojs/store': link:../../astro-store
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
svelte: 3.48.0
|
||||
|
@ -1910,6 +1937,7 @@ importers:
|
|||
|
||||
packages/integrations/vue:
|
||||
specifiers:
|
||||
'@astrojs/store': workspace:*
|
||||
'@vitejs/plugin-vue': ^2.3.3
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
|
@ -1919,6 +1947,7 @@ importers:
|
|||
'@vitejs/plugin-vue': 2.3.3_vite@2.9.10+vue@3.2.37
|
||||
vite: 2.9.10
|
||||
devDependencies:
|
||||
'@astrojs/store': link:../../astro-store
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
vue: 3.2.37
|
||||
|
|
Loading…
Add table
Reference in a new issue