@ryupold/vode 1.8.8 → 1.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.8.8",
3
+ "version": "1.8.10",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -25,11 +25,10 @@
25
25
  },
26
26
  "homepage": "https://github.com/ryupold/vode#readme",
27
27
  "type": "module",
28
- "main": "./dist/vode.mjs",
29
- "types": "./dist/vode.d.ts",
30
28
  "exports": {
31
29
  ".": {
32
30
  "types": "./dist/vode.d.ts",
31
+ "tests": "./dist/vode.tests.mjs",
33
32
  "import": "./dist/vode.min.mjs",
34
33
  "require": "./dist/vode.cjs.min.js",
35
34
  "default": "./dist/vode.min.js"
@@ -43,11 +42,12 @@
43
42
  "build-classic-min": "esbuild index.ts --outfile=dist/vode.min.js --bundle --format=iife --global-name=V --minify",
44
43
  "babel": "npx babel dist/vode.mjs --out-file dist/vode.cjs.min.js",
45
44
  "babel-classic": "npx babel dist/vode.js --out-file dist/vode.es5.min.js",
46
- "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types",
45
+ "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types && npm run build-tests",
47
46
  "publish": "npm publish --access public",
48
47
  "clean": "tsc -b --clean && rm dist/*",
49
48
  "watch": "tsc -b -w",
50
- "test": "tsc -p tsconfig.test.json && esbuild test/index.ts --bundle --outfile=test/bundle.js --platform=node && node test/bundle.js"
49
+ "test": "tsc -p tsconfig.test.json && esbuild test/run-tests.ts --bundle --outfile=test/run-tests.js --platform=node && node test/run-tests.js",
50
+ "build-tests": "esbuild test/index.ts --bundle --format=esm --outfile=dist/vode.tests.mjs"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@babel/cli": "7.28.6",
package/src/vode.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type Vode<S = PatchableState> = FullVode<S> | JustTagVode | NoPropsVode<S>;
2
- export type FullVode<S = PatchableState> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
2
+ export type FullVode<S = PatchableState> = [tag: Tag, props: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]];
3
3
  export type NoPropsVode<S = PatchableState> = [tag: Tag, ...children: ChildVode<S>[]] | (TextVode[]);
4
4
  export type JustTagVode = [tag: Tag];
5
5
  export type ChildVode<S = PatchableState> = Vode<S> | TextVode | NoVode | Component<S>;
@@ -37,9 +37,9 @@ export interface Props<S = PatchableState> extends Partial<
37
37
  class?: ClassProp,
38
38
  style?: StyleProp,
39
39
  /** called after the element was attached */
40
- onMount?: MountFunction<S>,
40
+ onMount?: MountFunction<S> | null | false,
41
41
  /** called before the element is detached */
42
- onUnmount?: MountFunction<S>,
42
+ onUnmount?: MountFunction<S> | null | false,
43
43
  /** used instead of original vode when an error occurs during rendering */
44
44
  catch?: ((s: S, error: Error) => ChildVode<S>) | ChildVode<S>;
45
45
  };
@@ -93,9 +93,8 @@ export interface ContainerNode<S = PatchableState> extends HTMLElement {
93
93
  renderAsync: () => Promise<unknown>,
94
94
  syncRenderer: (cb: () => void) => void,
95
95
  asyncRenderer: ((cb: () => void) => ViewTransition) | null | undefined,
96
- qSync: {} | undefined | null, // next patch aggregate to be applied
97
96
  qAsync: {} | undefined | null, // next render-patches to be animated after another
98
- isRendering: boolean,
97
+ isRendering: number,
99
98
  isAnimating: boolean,
100
99
  /** stats about the overall patches & last render time */
101
100
  stats: {
@@ -146,7 +145,7 @@ export function app<S extends PatchableState = PatchableState>(
146
145
  const _vode = {} as ContainerNode<S>["_vode"];
147
146
  _vode.syncRenderer = globals.requestAnimationFrame;
148
147
  _vode.asyncRenderer = globals.startViewTransition;
149
- _vode.qSync = null;
148
+ _vode.isRendering = 0;
150
149
  _vode.qAsync = null;
151
150
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
152
151
 
@@ -156,67 +155,78 @@ export function app<S extends PatchableState = PatchableState>(
156
155
  initialPatches = [...(state as any).patch.initialPatches, ...initialPatches];
157
156
  }
158
157
 
159
- Object.defineProperty(state, "patch", {
160
- enumerable: false, configurable: true,
161
- writable: false, value: async (action: Patch<S>, isAsync?: boolean) => {
162
- if (!action || (typeof action !== "function" && typeof action !== "object")) return;
163
- _vode.stats.patchCount++;
158
+ async function promisePatch(action: Promise<Patch<S>>, isAnimated?: boolean) {
159
+ _vode.stats.liveEffectCount++;
160
+ try {
161
+ const resolvedPatch = await (action as Promise<S>);
162
+ patchableState.patch(<Patch<S>>resolvedPatch, isAnimated);
163
+ } finally {
164
+ _vode.stats.liveEffectCount--;
165
+ }
166
+ }
164
167
 
165
- if ((action as AsyncGenerator<Patch<S>>)?.next) {
166
- const generator = action as AsyncGenerator<Patch<S>>;
168
+ async function generatorPatch(action: AsyncGenerator<Patch<S>>, isAnimated?: boolean) {
169
+ const generator = action as AsyncGenerator<Patch<S>>;
170
+ _vode.stats.liveEffectCount++;
171
+ try {
172
+ let v = await generator.next();
173
+ while (v.done === false) {
167
174
  _vode.stats.liveEffectCount++;
168
175
  try {
169
- let v = await generator.next();
170
- while (v.done === false) {
171
- _vode.stats.liveEffectCount++;
172
- try {
173
- patchableState.patch(v.value, isAsync);
174
- v = await generator.next();
175
- } finally {
176
- _vode.stats.liveEffectCount--;
177
- }
178
- }
179
- patchableState.patch(v.value as Patch<S>, isAsync);
176
+ patchableState.patch(v.value, isAnimated);
177
+ v = await generator.next();
180
178
  } finally {
181
179
  _vode.stats.liveEffectCount--;
182
180
  }
181
+ }
182
+ patchableState.patch(v.value as Patch<S>, isAnimated);
183
+ } finally {
184
+ _vode.stats.liveEffectCount--;
185
+ }
186
+ }
187
+
188
+ Object.defineProperty(state, "patch", {
189
+ enumerable: false, configurable: true,
190
+ writable: false, value: (action: Patch<S>, isAnimated?: boolean) => {
191
+ while (typeof action === "function") {
192
+ action = (<(s: S) => unknown>action)(_vode.state);
193
+ }
194
+
195
+ if (!action || typeof action !== "object") return;
196
+
197
+ _vode.stats.patchCount++;
198
+
199
+ if ((action as AsyncGenerator<Patch<S>>)?.next) {
200
+ generatorPatch(action as AsyncGenerator<Patch<S>>, isAnimated);
183
201
  } else if ((action as Promise<S>).then) {
184
- _vode.stats.liveEffectCount++;
185
- try {
186
- const resolvedPatch = await (action as Promise<S>);
187
- patchableState.patch(<Patch<S>>resolvedPatch, isAsync);
188
- } finally {
189
- _vode.stats.liveEffectCount--;
190
- }
202
+ promisePatch(action as Promise<S>, isAnimated);
191
203
  } else if (Array.isArray(action)) {
192
204
  if (action.length > 0) {
193
205
  for (const p of action) {
194
206
  patchableState.patch(p, !document.hidden && !!_vode.asyncRenderer);
195
207
  }
196
208
  } else { //when [] is patched: 1. skip current animation 2. merge all queued async patches into synced queue
197
- _vode.qSync = mergeState(_vode.qSync || {}, _vode.qAsync, false);
209
+ mergeState(_vode.state, _vode.qAsync, true);
198
210
  _vode.qAsync = null;
199
211
  try { globals.currentViewTransition?.skipTransition(); } catch { }
200
212
  _vode.stats.syncRenderPatchCount++;
201
213
  _vode.renderSync();
202
214
  }
203
- } else if (typeof action === "function") {
204
- patchableState.patch((<(s: S) => unknown>action)(_vode.state), isAsync);
205
215
  } else {
206
- if (isAsync) {
216
+ if (isAnimated) {
207
217
  _vode.stats.asyncRenderPatchCount++;
208
218
  _vode.qAsync = mergeState(_vode.qAsync || {}, action, false);
209
- await _vode.renderAsync();
219
+ _vode.renderAsync();
210
220
  } else {
211
221
  _vode.stats.syncRenderPatchCount++;
212
- _vode.qSync = mergeState(_vode.qSync || {}, action, false);
222
+ mergeState(_vode.state, action, true);
213
223
  _vode.renderSync();
214
224
  }
215
225
  }
216
226
  }
217
227
  });
218
228
 
219
- function renderDom(isAsync: boolean) {
229
+ function renderDom(isAnimated: boolean) {
220
230
  const sw = performance.now();
221
231
  const vom = dom(_vode.state);
222
232
  _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
@@ -226,11 +236,13 @@ export function app<S extends PatchableState = PatchableState>(
226
236
  (<ContainerNode<S>>container)._vode = _vode
227
237
  }
228
238
 
229
- if (!isAsync) {
239
+ if (!isAnimated) {
230
240
  _vode.stats.lastSyncRenderTime = performance.now() - sw;
241
+ const changesSinceRender = _vode.isRendering !== _vode.stats.syncRenderPatchCount;
231
242
  _vode.stats.syncRenderCount++;
232
- _vode.isRendering = false;
233
- if (_vode.qSync) _vode.renderSync();
243
+ _vode.isRendering = 0;
244
+ if (changesSinceRender)
245
+ _vode.renderSync();
234
246
  }
235
247
  }
236
248
  const sr = renderDom.bind(null, false);
@@ -239,13 +251,9 @@ export function app<S extends PatchableState = PatchableState>(
239
251
  Object.defineProperty(_vode, "renderSync", {
240
252
  enumerable: false, configurable: true,
241
253
  writable: false, value: () => {
242
- if (_vode.isRendering || !_vode.qSync) return;
243
-
244
- _vode.isRendering = true;
245
-
246
- _vode.state = mergeState(_vode.state, _vode.qSync, true);
247
- _vode.qSync = null;
254
+ if (_vode.isRendering) return;
248
255
 
256
+ _vode.isRendering = _vode.stats.syncRenderPatchCount;
249
257
  _vode.syncRenderer(sr);
250
258
  }
251
259
  });
@@ -281,7 +289,8 @@ export function app<S extends PatchableState = PatchableState>(
281
289
  root._vode = _vode;
282
290
  const indexInParent = Array.from(container.parentElement.children).indexOf(container);
283
291
 
284
- _vode.isRendering = true;
292
+ const patchCountBefore = _vode.stats.syncRenderPatchCount;
293
+ _vode.isRendering = _vode.stats.syncRenderPatchCount;
285
294
  _vode.vode = render(
286
295
  <S>state,
287
296
  container.parentElement,
@@ -290,8 +299,10 @@ export function app<S extends PatchableState = PatchableState>(
290
299
  hydrate<S>(container, true) as AttachedVode<S>,
291
300
  dom(<S>state)
292
301
  )!;
293
- _vode.isRendering = false;
294
- if (_vode.qSync) _vode.renderSync();
302
+ const continueRendering = _vode.stats.syncRenderPatchCount !== patchCountBefore;
303
+ _vode.isRendering = 0;
304
+ _vode.stats.syncRenderCount++;
305
+ if (continueRendering) _vode.renderSync();
295
306
 
296
307
  for (const effect of initialPatches) {
297
308
  patchableState.patch(effect);