From aa03e0cbc9e6e6f8c1db35a30a6ad6376dc0a404 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 19 Mar 2024 16:36:30 -0400 Subject: [PATCH] chore: remove examples since they are out of date with the latest spec --- examples/moby/README.md | 23 -- examples/moby/index.html | 11 - examples/moby/src/coloring.md | 108 --------- examples/moby/src/example.test.ts | 368 ------------------------------ examples/moby/src/example.ts | 50 ---- examples/moby/src/signals.ts | 357 ----------------------------- examples/moby/start.sh | 3 - examples/moby/vite.config.js | 5 - 8 files changed, 925 deletions(-) delete mode 100644 examples/moby/README.md delete mode 100644 examples/moby/index.html delete mode 100644 examples/moby/src/coloring.md delete mode 100644 examples/moby/src/example.test.ts delete mode 100644 examples/moby/src/example.ts delete mode 100644 examples/moby/src/signals.ts delete mode 100755 examples/moby/start.sh delete mode 100644 examples/moby/vite.config.js diff --git a/examples/moby/README.md b/examples/moby/README.md deleted file mode 100644 index 1112141..0000000 --- a/examples/moby/README.md +++ /dev/null @@ -1,23 +0,0 @@ - -# MobY - -MobX inspired mobx Signal implementation - -## Differences with MobX - -* Nested effects are supported, including cleanup example, based on Dominic's work in exasmple-a -* Unlike MobX, effects are scheduled async - -## Differences with spec? - -* Effects do not return a value - * Could be, chose not to, to not conflate semantic clarity with Computeds. E.g. effect.get() is not a tracked value - * A run can be forced by calling `.run()` -* Effect lifecycle - * `notify()` is called sync - * `notify()` should at some point result in `.run()` call. - * However, effect execution and dirty checking can be separated, by calling `isDirty()` earlier than `run()`. - * This allows to establish different moments in which deps are checked and the tracking is run. - * For example, for a normal `effect` we want to run `run` asap, when deps have changed, and no further schedulig is required - * For react integration, if deps have changed (for sure), we want to notify React, but only track as part of the next as part of Reacts component lifecycle, rather than as part of the effect lifecycle. In React, we want to trigger a `isDirty()` check on the next tick from `notify`. However, if `dirty`, we don't want to run the effect immediately, but rather schedule a React render, which then in turn invokes `run - * `isDirty` (deps have surely changed) as a distinction between `notify` (maybe deps have changed), to not force computed values to be evaluated too early diff --git a/examples/moby/index.html b/examples/moby/index.html deleted file mode 100644 index 923b78e..0000000 --- a/examples/moby/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Signals Example - - - -

Signals Example

- - - diff --git a/examples/moby/src/coloring.md b/examples/moby/src/coloring.md deleted file mode 100644 index 16c3f9b..0000000 --- a/examples/moby/src/coloring.md +++ /dev/null @@ -1,108 +0,0 @@ -# moby - - -## Batching strategies - -### (1) Color & compute early, schedule (push / push) - -Do a topological sort in which computed are updated in order and synchronously, and reactions are scheduled if any of their deps have truly changed. - -1. propagate dirty state -2. propagate changed state, computeds inmediately run in this phase and recursively pass on (un)changed state -3. if a reaction received a changed state, it'll schedule and run - -Considerations: - -1. (+) reactions are only scheduled if some dep actually changed -1. (+) reactions can run sync -1. (-) After every signal mutation, the full computation graph is recomputed, without explicit batch API is perf kill - -### (2) Color, compute in microtask, schedule (push / push async) - -Do a topological sort, and in a microtask computeds are updated in order and synchronously, and reactions are scheduled if any of their deps have truly changed. - -1. propagate dirty state, schedule microtask to process affected computeds -2. computeds are re-evaluated in the microtask, bringing the full dep graph up to date -3. if a reaction received a changed dep, it'll schedule and run - -Considerations: - -1. (+) reactions are only scheduled if some dep actually changed -1. (+) implicit batching avoid recomputation -1. (-) reactions cannot run sync -1. (-) in every microtask, the full computation graph is updated, which might be more often than some reactions require if they'll only run in a while - -### (3) Color & schedule, compute late (push / pull) - -Propagate dirty state inmediately, schedule all affected reactions, than lazily pull all values through the system. - -1. propagate dirty state -2. schedule any affected reaction -3. during run pull values through the graph - -Considerations: - -1. (+) no computations are eager -1. (+) implicit batching for free -1. (+) reactions can run sync -1. (-) all _potentially_ affected reactions are scheduled and run. E.g. every React component _will_ render - -### (4) Color & partially compute early, batch, schedule (push / (push / pull)) (MobX) - -Using an explict batch API, propagate dirty state fully, than check all candidate reactions at the end of the batch for at least one changed dep (pulled) before scheduling. - -1. mark transactions explicitly -2. propagate dirty state -2. mark all potentially affected reactions as potential candidates at end of batch -3. once batch end, for each reaction, pull dependencies of the reaction one by one, on the first change detected, abort the process and schedule the reaction. The other deps are pulled lazily. - -Considerations - -1. (-) Part of the computation graph runs early, but only at the end of the batch -2. (+) only truly affected reactions are scheduled - -### (5) Color & partially compute on microtask, schedule (push / (push / pull)) (MobY) - -Like (4), but instead of explicit batch api, do the end-of-batch work in a next microtask (or later depending on the Reactions scheduler) -Reaction -> schedule staleness check -> schedule run&track - -1. schedule microtask -2. propagate dirty state -2. mark all affected reactions as potential candidates for the microtask -3. once batch end, pull dependencies of reactions one by one, on the first change, abort the process and schedule the reaction - -Considerations - -1. (-) Part of the computation graph runs early in the microtask, but only at the end of the implicit batch -1. (-) drops the possibility of ever running a reation truly synchronous -2. (+) only truly affected reactions are scheduled - -### (6) Always schedule all reactions, only compute on pull (pull / pull) (starbeam?) - -Always assume all reactions have changed, in a microtask pull all deps, schedule effect (or schedule immediately) - -1. No change propagate, but increase logical clock for mutated signals -1. schedule microtask -1. for all reactions, pull their deps, schedule reaction if affected - -Considerations - -1. (+) signals need no notion of their observers -2. (-) full graph is always re-evaluated - -## Setup - -```bash -# install dependencies -bun install - -# test the app -bun test - -# build the app, available under dist -bun run build -``` - -## License - -MIT diff --git a/examples/moby/src/example.test.ts b/examples/moby/src/example.test.ts deleted file mode 100644 index 0400aa0..0000000 --- a/examples/moby/src/example.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { expect, test } from "vitest"; -import {Signal, effect} from "./signals"; - -const nextTick = () => Promise.resolve(); - -export function syncValueEffect(fn: () => T): { - current: T, - dispose(): void -} { - let current: T - const signal = new Signal.Effect(function () { - current = fn(); - }, { - notify() { - this.run() - } - }); - - return { - get current() { - return current; - }, - dispose() { - signal[Symbol.dispose]() - } - } -} - - -test("state signal can be written and read from", () => { - const a = new Signal.State(0); - - expect(a.get()).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); -}); - -test("computed signal can be read from", () => { - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - - expect(a.get()).toBe(0); - expect(b.get()).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); - expect(b.get()).toBe(2); -}); - -test("effect signal can be read from", () => { - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - const c = syncValueEffect(() => b.get()); - - expect(a.get()).toBe(0); - expect(b.get()).toBe(0); - expect(c.current).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); - expect(b.get()).toBe(2); - expect(c.current).toBe(2); -}); - -test("effect signal can notify changes", async () => { - let is_dirty = false; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 0); - - let value; - // TODO: different from example-a! - const c = new Signal.Effect(() => {value = b.get();}, { - notify: () => (is_dirty = true), - }); - - expect(value).toBe(0); - expect(is_dirty).toBe(false); - - a.set(1); - - c[Symbol.dispose](); - - await nextTick(); - - expect(is_dirty).toBe(true); - - is_dirty = false; - - a.set(1); - - await nextTick(); - - // State hasn't changed - expect(is_dirty).toBe(false); - - a.set(2); - - // Computed hasn't changed - expect(is_dirty).toBe(false); -}); - -test("example framework effect signals can be nested", () => { - let log: string[] = []; - const l = (m: string) => { - log.push(m); - // console.log(m); - } - - const a = new Signal.State(0); - const b = effect( - () => { - l("b update " + a.get()); - const c = effect( - () => { - l("c create " + a.get()); - - return () => { - l("c cleanup " + a.get()); - }; - }, - ); - // c.get(); - - return () => { - l("b cleanup " + a.get()); - }; - }, - ); - - b.run() - - a.set(1); - b.run() - - a.set(2); - - b[Symbol.dispose](); - - expect(log).toEqual([ - "b update 0", - "c create 0", - "c cleanup 1", - "b cleanup 1", - "b update 1", - "c create 1", - "c cleanup 2", - "b cleanup 2", - ]); -}); - -test("effect signal should trigger oncleanup and correctly disconnect from graph", () => { - let cleanups: string[] = []; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 0); - const c = new Signal.Effect(() => { - b.get() - return () => { - cleanups.push("c"); - } - }, { - notify: () => {}, - }); - - expect(cleanups).toEqual([]); - - c.run(); - - expect(a.sinks?.size).toBe(1); - expect(b.sinks?.size).toBe(1); - expect(cleanups).toEqual([]); - - c[Symbol.dispose](); - - expect(a.sinks.size).toBe(0); - expect(b.sinks.size).toBe(0); - expect(cleanups).toEqual(["c"]); -}); - -test("effect signal should propogate correctly with computed signals", () => { - let log: string[] = []; - let effects: Signal.Effect[] = []; - - const queueEffect = (signal: Signal.Effect) => { - effects.push(signal); - }; - - const flush = () => { - effects.forEach((e) => e.run()); - effects = []; - }; - - const count = new Signal.State(0); - const double = new Signal.Computed(() => count.get() * 2); - const triple = new Signal.Computed(() => count.get() * 3); - const quintuple = new Signal.Computed(() => double.get() + triple.get()); - - const a = new Signal.Effect( - () => { - log.push("four"); - log.push( - `${count.get()}:${double.get()}:${triple.get()}:${quintuple.get()}` - ); - }, - { - notify: queueEffect, - } - ); - a.run(); - const b = new Signal.Effect( - () => { - log.push("three"); - log.push(`${double.get()}:${triple.get()}:${quintuple.get()}`); - }, - { - notify: queueEffect, - } - ); - b.run(); - const c = new Signal.Effect( - () => { - log.push("two"); - log.push(`${count.get()}:${double.get()}`); - }, - { - notify: queueEffect, - } - ); - c.run(); - const d = new Signal.Effect( - () => { - log.push("one"); - log.push(`${double.get()}`); - }, - { - notify: queueEffect, - } - ); - d.run(); - - expect(log).toEqual([ - "four", - "0:0:0:0", - "three", - "0:0:0", - "two", - "0:0", - "one", - "0", - ]); - - log = []; - - count.set(1); - flush(); - - expect(log).toEqual([ - "four", - "1:2:3:5", - "three", - "2:3:5", - "two", - "1:2", - "one", - "2", - ]); - - a[Symbol.dispose](); - b[Symbol.dispose](); - c[Symbol.dispose](); - d[Symbol.dispose](); -}); - -test("effect signal should notify only once", () => { - let log: string[] = []; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - const c = new Signal.Effect( - () => { - a.get(); - b.get(); - log.push("effect ran"); - }, - { - notify: () => { - log.push("notified"); - }, - } - ); - - expect(log).toEqual(["effect ran"]); - - a.set(1); - a.set(2); - - expect(log).toEqual(["effect ran", "notified"]); - - c[Symbol.dispose](); -}); - -test("https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn", async () => { - let res: number[] = []; - - const numbers = Array.from({ length: 2 }, (_, i) => i); - const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2)); - const hard = (n: number, l: string) => n + fib(16); - - const A = new Signal.State(0); - const B = new Signal.State(0); - const C = new Signal.Computed(() => (A.get() % 2) + (B.get() % 2)); - const D = new Signal.Computed( - () => numbers.map((i) => i + (A.get() % 2) - (B.get() % 2)), - { equals: (l, r) => l.length === r.length && l.every((v, i) => v === r[i]) } - ); - const E = new Signal.Computed(() => - hard(C.get() + A.get() + D.get()[0]!, "E") - ); - const F = new Signal.Computed(() => hard(D.get()[0]! && B.get(), "F")); - const G = new Signal.Computed( - () => C.get() + (C.get() || E.get() % 2) + D.get()[0]! + F.get() - ); - let H = effect( - () => { - res.push(hard(G.get(), "H")); - }, - ); - let I = effect( - () => { - res.push(G.get()); - }, - - ); - let J = effect( - () => { - res.push(hard(F.get(), "J")); - }, - - ); - - // H.get(); - // I.get(); - // J.get(); - - let i = 2; - while (--i) { - res.length = 0; - B.set(1); - A.set(1 + i * 2); - await nextTick(); - - A.set(2 + i * 2); - B.set(2); - await nextTick(); - - expect(res.length).toBe(4); - expect(res).toEqual([3198, 1601, 3195, 1598]); - } - - H[Symbol.dispose](); - I[Symbol.dispose](); - J[Symbol.dispose](); -}); diff --git a/examples/moby/src/example.ts b/examples/moby/src/example.ts deleted file mode 100644 index 690d0ec..0000000 --- a/examples/moby/src/example.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Signal, effect} from "./signals"; - -// Setup UI - -const container = document.createElement("div"); -const element = document.createElement("div"); -const button = document.createElement("button"); -button.innerText = "Increment number"; -container.appendChild(element); -container.appendChild(button); -document.body.appendChild(container); - -// Example taken from README.md - -const counter = new Signal.State(0); -const isEven = new Signal.Computed(() => (counter.get() & 1) == 0); -const parity = new Signal.Computed(() => (isEven.get() ? "even" : "odd")); - -effect(() => { - element.innerText = `Counter: ${counter.get()}\nParity: ${parity.get()}`; -}); - -effect(() => { - Signal.unsafe.untrack(() => { - counter.set(counter.get() + 1); - }); -}); - -// Expanded example showing how effects work - -effect(() => { - console.log("update main effect", counter.get()); - - // Nested effects - effect(() => { - console.log("create nested effect"); - - return () => { - console.log("cleanup nested effect"); - }; - }); - - return () => { - console.log("cleanup main effect"); - }; -}); - -button.addEventListener("click", () => { - counter.set(counter.get() + 1); -}); diff --git a/examples/moby/src/signals.ts b/examples/moby/src/signals.ts deleted file mode 100644 index b701505..0000000 --- a/examples/moby/src/signals.ts +++ /dev/null @@ -1,357 +0,0 @@ -interface Signal { - _version: number; - sinks: Set; - get(): T; - _addSubscriber(subscriber: Subscriber): void; - // TODO: implement removeals similar to https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/graph.ts#L421 - _removeSubscriber(subscriber: Subscriber): void; -} - -type SignalComparison = (/*this: Signal, */ t: T, t2: T) => boolean; - -interface SignalOptions { - // Custom comparison function between old and new value. Default: Object.is. - equals?: SignalComparison; -} - -interface ComputedOptions extends SignalOptions { } - -enum DirtyState { - Clean, - MaybeDirty, - Dirty -} // TODO: distinction needed? - -interface Subscriber { - deps: Set>; - _dirty: DirtyState; - _version: number; - _markStale(): void; - _markMaybeStale(): void; -} - -type EffectReturn = void | (() => void); - -let version = 0; - -let currentlyTracking: null | { - subscriber: Subscriber; - deps: Set>; -}; - -let pendingReactions: null | Signal.Effect[] = null; - -function scheduleNextTick(effect: Signal.Effect) { - if (pendingReactions === null) { - pendingReactions = [effect]; - scheduleInvalidateReactionsInMicrotask(); - } else { - pendingReactions.push(effect); - } -} - -function scheduleInvalidateReactionsInMicrotask() { - queueMicrotask(() => { - const toInvalidate = pendingReactions; - pendingReactions = null; // unset first to be able to schedule another microtask if needed - // In future: build loop protection - invariant(toInvalidate); // should never be empty! - toInvalidate.forEach((r) => { - r.run(); - }); - }); -} - - -export namespace Signal { - // A read-write Signal - export class State implements Signal { - sinks = new Set(); - _version = version; - _dirty: DirtyState = DirtyState.Clean; - - // Create a state Signal starting with the value t - constructor(private _value: T, private options?: SignalOptions) { } - - get(): T { - markObserved(this); - return this._value; - } - - // Set the state Signal value to t - set(t: T): void { - invariant( - !(currentlyTracking?.subscriber instanceof Computed), - "Computed values cannot write as side effect" - ); - if (!isEqual(this.options, this._value, t)) { - this._value = t; - this._version = ++version; - // propagate changed - this.sinks.forEach((s) => { - // Optimize: no forEach - s._markStale(); - }); - } - } - - _addSubscriber(subscriber: Subscriber): void { - this.sinks.add(subscriber); - } - _removeSubscriber(subscriber: Subscriber): void { - this.sinks.delete(subscriber); - } - } - - // A Signal which is a formula based on other Signals - export class Computed implements Signal, Subscriber { - sinks = new Set(); - deps = new Set>(); - - // By default we mark a computed as dirty! - _dirty: DirtyState = DirtyState.Dirty; - _version = -1; - _value!: T; - private isTracked = false; - - // Create a Signal which evaluates to the value of cb.call(the Signal) - constructor( - private compute: (this: Computed) => T, - private options?: ComputedOptions - ) { } - - get(): T { - markObserved(this); - if (!this.isTracked) { - this.isTracked = currentlyTracking !== null; - } - // if the world didn't change since last read, we're fine anyway - if (version !== this._version && shouldRecompute(this)) { - // if this computed is untracked, we won't have backrefs notifying the state of this thing, - // so we have to always check whether some dep version has changed! - this._dirty = this.isTracked ? DirtyState.Clean : DirtyState.MaybeDirty; - const newValue = track(this, this.compute, this.isTracked); - if (this._version === -1 || !isEqual(this.options, this._value, newValue)) { - this._value = newValue; - this._version = version; - } - } - return this._value; - } - - _markStale() { - const startState = this._dirty - this._dirty = DirtyState.Dirty; - if (startState === DirtyState.Clean) { - this.sinks.forEach((s) => s._markMaybeStale()); - } - } - - _markMaybeStale() { - if (this._dirty === DirtyState.Clean) { - this._dirty = DirtyState.MaybeDirty; - this.sinks.forEach((s) => s._markMaybeStale()); - } - // else: already Dirty or MaybeDirty - } - - _addSubscriber(subscriber: Subscriber): void { - invariant(this.isTracked); // someone should have read the value in a reactive context - this.sinks.add(subscriber); - } - _removeSubscriber(subscriber: Subscriber): void { - this.sinks.delete(subscriber); - if (this.sinks.size === 0) { - this.isTracked = false; - // From this point onward, we can no longer assume annything - this._dirty = DirtyState.MaybeDirty; - // unsubscribe from our deps - this.deps.forEach((dep) => { - dep._removeSubscriber(this); - }); - } - } - } - - interface EffectOptions { - notify?: (this: Effect, signal: Effect) => void; - } - - export class Effect implements Subscriber { - _dirty: DirtyState = DirtyState.Dirty; - _version = -1; - _disposed = false; - _parent?: Effect; - _children = new Set() // optmization: lazy init - deps: Set> = new Set(); - _cleanup :EffectReturn - - // schedule should at some time call check - // this.hasChangedDeps(), and if true, call directly or later - // this.track(() => { } ... - constructor(public effect: (this: Effect) => EffectReturn, private options?: EffectOptions) { - const subscriber = currentlyTracking?.subscriber; - if (subscriber) { - invariant(subscriber instanceof Effect, "Only Reactions can create other Reactions"); - subscriber._children.add(this); - this._parent = subscriber; - } - this.run(); - } - - _markStale() { - let startState = this._dirty; - this._dirty = DirtyState.Dirty - if (startState === DirtyState.Clean) { - this._scheduleInvalidation(); - } - } - - _markMaybeStale() { - if (this._dirty === DirtyState.Clean) { - this._dirty = DirtyState.MaybeDirty; - this._scheduleInvalidation(); - } - // else: already Dirty or MaybeDirty - } - - _scheduleInvalidation() { - if (this._disposed) { - return; - } - if (this.options?.notify) { - this.options?.notify.call(this, this); - } else { - scheduleNextTick(this); - } - } - - isDirty(): boolean { - if (this._disposed) { - return false; - } - return shouldRecompute(this); - } - - run(): void { - if (this._disposed) return; - // TODO: this should be checked for all parents recursively, not just the first - if (this.isDirty() && (this._version === -1 || this._parent === undefined || !this._parent.isDirty())) { - this._runCleanup(); - // TODO: error handling and such - this._cleanup = track(this, this.effect, true); - // store against which state version we did run - } - - this._version = version; - this._dirty = DirtyState.Clean - } - - _runCleanup() { - this._children.forEach((r) => { - r._parent = undefined; - r[Symbol.dispose]() - }) - this._children.clear(); - let cleanup = this._cleanup; - this._cleanup = undefined; - cleanup?.(); - } - - [Symbol.dispose]() { - this._disposed = true; - // TODO: set flag that we have disposed? - this.deps.forEach((d) => d._removeSubscriber(this)); - this.deps.clear(); - // remove the back reference if any - this._parent?._children.delete(this); - // run own cleanup lat - this._runCleanup(); - } - } - - export namespace unsafe { - export function untrack(cb: () => T): T { - let prevTracking = currentlyTracking; - currentlyTracking = null; - try { - return cb(); - } finally { - currentlyTracking = prevTracking; - } - } - } -} - -function shouldRecompute(subscriber: Subscriber): boolean { - if (subscriber._dirty === DirtyState.MaybeDirty) { - subscriber._dirty = DirtyState.Clean; // Unless proven otherwise - for (const dep of subscriber.deps) { - // Optimization: Like MobX store lowestObservableState or loop the collection twice to see first if there is a higher version instead of MaybeDIrty - if (dep instanceof Signal.Computed) { - dep.get(); // force a recomputation of the value - } - if (dep._version > subscriber._version) { - subscriber._dirty = DirtyState.Dirty; // cache the result, to avoid pulling another time - break; - } - } - } - return !!subscriber._dirty; // clean -> 0 -> false, dirty -> 1 -> true -} - -function track( - subscriber: S, - fn: (this: S) => T, - storeBackRefs: boolean -): T { - // TODO: error handling and such - const origDeps = subscriber.deps; - const prevTracking = currentlyTracking; - const deps = new Set>(); - currentlyTracking = { - deps, - subscriber - }; - try { - return fn.call(subscriber); - } finally { - subscriber._dirty = DirtyState.Clean; - subscriber.deps = deps; - // TODO: more efficient diffing with arrays like in MobX - if (storeBackRefs) { - for (const dep of deps) { - if (!origDeps.has(dep)) dep._addSubscriber(subscriber); - } - for (const dep of origDeps) { - if (!deps.has(dep)) dep._removeSubscriber(subscriber); - } - } - currentlyTracking = prevTracking; - } -} - -function markObserved(signal: Signal): void { - currentlyTracking?.deps.add(signal); // Optimize: reuse MobX's lastObservedBy strategy -} - -function invariant( - condition: any, - msg = "Invariant failure" -): asserts condition { - if (!condition) { - throw new Error(msg); - } -} - -function isEqual( - options: undefined | SignalOptions, - a: T, - b: T -): boolean { - return (options?.equals ?? Object.is)(a, b); -} - -export function effect(fn: () => EffectReturn) { - return new Signal.Effect(fn); -} diff --git a/examples/moby/start.sh b/examples/moby/start.sh deleted file mode 100755 index 563a5a9..0000000 --- a/examples/moby/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -npx vite diff --git a/examples/moby/vite.config.js b/examples/moby/vite.config.js deleted file mode 100644 index 2e63ffc..0000000 --- a/examples/moby/vite.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - // config here -})