@ryupold/vode 1.8.10 → 1.8.11

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/dist/vode.mjs CHANGED
@@ -28,7 +28,7 @@ function app(container, state, dom, ...initialPatches) {
28
28
  _vode.stats.liveEffectCount++;
29
29
  try {
30
30
  const resolvedPatch = await action;
31
- patchableState.patch(resolvedPatch, isAnimated);
31
+ await patchableState.patch(resolvedPatch, isAnimated);
32
32
  } finally {
33
33
  _vode.stats.liveEffectCount--;
34
34
  }
@@ -41,13 +41,13 @@ function app(container, state, dom, ...initialPatches) {
41
41
  while (v.done === false) {
42
42
  _vode.stats.liveEffectCount++;
43
43
  try {
44
- patchableState.patch(v.value, isAnimated);
44
+ await patchableState.patch(v.value, isAnimated);
45
45
  v = await generator.next();
46
46
  } finally {
47
47
  _vode.stats.liveEffectCount--;
48
48
  }
49
49
  }
50
- patchableState.patch(v.value, isAnimated);
50
+ await patchableState.patch(v.value, isAnimated);
51
51
  } finally {
52
52
  _vode.stats.liveEffectCount--;
53
53
  }
@@ -63,9 +63,9 @@ function app(container, state, dom, ...initialPatches) {
63
63
  if (!action || typeof action !== "object") return;
64
64
  _vode.stats.patchCount++;
65
65
  if (action?.next) {
66
- generatorPatch(action, isAnimated);
66
+ return generatorPatch(action, isAnimated);
67
67
  } else if (action.then) {
68
- promisePatch(action, isAnimated);
68
+ return promisePatch(action, isAnimated);
69
69
  } else if (Array.isArray(action)) {
70
70
  if (action.length > 0) {
71
71
  for (const p of action) {
@@ -28,7 +28,7 @@ function app(container, state, dom, ...initialPatches) {
28
28
  _vode.stats.liveEffectCount++;
29
29
  try {
30
30
  const resolvedPatch = await action;
31
- patchableState.patch(resolvedPatch, isAnimated);
31
+ await patchableState.patch(resolvedPatch, isAnimated);
32
32
  } finally {
33
33
  _vode.stats.liveEffectCount--;
34
34
  }
@@ -41,13 +41,13 @@ function app(container, state, dom, ...initialPatches) {
41
41
  while (v.done === false) {
42
42
  _vode.stats.liveEffectCount++;
43
43
  try {
44
- patchableState.patch(v.value, isAnimated);
44
+ await patchableState.patch(v.value, isAnimated);
45
45
  v = await generator.next();
46
46
  } finally {
47
47
  _vode.stats.liveEffectCount--;
48
48
  }
49
49
  }
50
- patchableState.patch(v.value, isAnimated);
50
+ await patchableState.patch(v.value, isAnimated);
51
51
  } finally {
52
52
  _vode.stats.liveEffectCount--;
53
53
  }
@@ -63,9 +63,9 @@ function app(container, state, dom, ...initialPatches) {
63
63
  if (!action || typeof action !== "object") return;
64
64
  _vode.stats.patchCount++;
65
65
  if (action?.next) {
66
- generatorPatch(action, isAnimated);
66
+ return generatorPatch(action, isAnimated);
67
67
  } else if (action.then) {
68
- promisePatch(action, isAnimated);
68
+ return promisePatch(action, isAnimated);
69
69
  } else if (Array.isArray(action)) {
70
70
  if (action.length > 0) {
71
71
  for (const p of action) {
@@ -1206,19 +1206,25 @@ ${other}${failSuffix}`);
1206
1206
  waitTimeMs
1207
1207
  );
1208
1208
  }
1209
- toSucceed() {
1209
+ toSucceed(failMessage) {
1210
+ const failSuffix = failMessage ? `
1211
+
1212
+ ${failMessage}` : "";
1210
1213
  if (typeof this.what !== "function") {
1211
1214
  throw new ExpectationError(this, `expected a function
1212
1215
 
1213
- but it is a ${typeof this.what}`);
1216
+ but it is a ${typeof this.what}${failSuffix}`);
1214
1217
  }
1215
1218
  return this.what();
1216
1219
  }
1217
- toFail() {
1220
+ toFail(failMessage) {
1221
+ const failSuffix = failMessage ? `
1222
+
1223
+ ${failMessage}` : "";
1218
1224
  if (typeof this.what !== "function") {
1219
1225
  throw new ExpectationError(this, `expected a function
1220
1226
 
1221
- but it is a ${typeof this.what}`);
1227
+ but it is a ${typeof this.what}${failSuffix}`);
1222
1228
  }
1223
1229
  let r;
1224
1230
  try {
@@ -1230,25 +1236,34 @@ but it is a ${typeof this.what}`);
1230
1236
 
1231
1237
  but it succeeded with a result of type ${typeof r}
1232
1238
 
1233
- ${r}`);
1239
+ ${r}${failSuffix}`);
1234
1240
  }
1235
- toSucceedAsync(waitTime = 100) {
1241
+ toSucceedAsync(failMessage, waitTime = 100) {
1242
+ const failSuffix = failMessage ? `
1243
+
1244
+ ${failMessage}` : "";
1236
1245
  if (typeof this.what !== "function") {
1237
1246
  throw new ExpectationError(this, `expected a function
1238
1247
 
1239
- but it is a ${typeof this.what}`);
1248
+ but it is a ${typeof this.what}${failSuffix}`);
1240
1249
  }
1241
1250
  return retry(() => this.what(), waitTime);
1242
1251
  }
1243
- async toFailAsync() {
1252
+ async toFailAsync(failMessage) {
1253
+ const failSuffix = failMessage ? `
1254
+
1255
+ ${failMessage}` : "";
1244
1256
  if (typeof this.what !== "function") {
1245
1257
  throw new ExpectationError(this, `expected a function
1246
1258
 
1247
- but it is a ${typeof this.what}`);
1259
+ but it is a ${typeof this.what}${failSuffix}`);
1248
1260
  }
1249
1261
  let r;
1250
1262
  try {
1251
- r = await this.what();
1263
+ if (typeof this.what === "function")
1264
+ r = await this.what();
1265
+ else
1266
+ r = await this.what;
1252
1267
  } catch (err) {
1253
1268
  return err;
1254
1269
  }
@@ -1256,7 +1271,7 @@ but it is a ${typeof this.what}`);
1256
1271
 
1257
1272
  but it succeeded with a result of type ${typeof r}
1258
1273
 
1259
- ${r}`);
1274
+ ${r}${failSuffix}`);
1260
1275
  }
1261
1276
  async toMatch(v, state, failMessage, waitTimeMs = 100) {
1262
1277
  return await retry(
@@ -2942,6 +2957,103 @@ var tests_mount_unmount_default = {
2942
2957
  patch({ show: true });
2943
2958
  await expect(mounts).toEqual(["mount span"]);
2944
2959
  },
2960
+ "onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
2961
+ const container = setup();
2962
+ const mounts = [];
2963
+ const broken = () => {
2964
+ throw new Error("boom");
2965
+ };
2966
+ app(
2967
+ container,
2968
+ {},
2969
+ () => [
2970
+ DIV,
2971
+ {
2972
+ catch: [
2973
+ SECTION,
2974
+ {
2975
+ onMount: (s, ele) => {
2976
+ mounts.push("mount fallback");
2977
+ }
2978
+ },
2979
+ "fallback"
2980
+ ]
2981
+ },
2982
+ broken
2983
+ ]
2984
+ );
2985
+ await expect(mounts).toEqual(["mount fallback"]);
2986
+ },
2987
+ "onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
2988
+ const container = setup();
2989
+ const mounts = [];
2990
+ const caughtErrors = [];
2991
+ const broken = () => {
2992
+ throw new Error("boom");
2993
+ };
2994
+ app(
2995
+ container,
2996
+ {},
2997
+ () => [
2998
+ DIV,
2999
+ {
3000
+ catch: (s, err) => {
3001
+ caughtErrors.push(err.message);
3002
+ return [
3003
+ SECTION,
3004
+ {
3005
+ onMount: (s2, ele) => {
3006
+ mounts.push("mount fallback");
3007
+ }
3008
+ },
3009
+ "fallback"
3010
+ ];
3011
+ }
3012
+ },
3013
+ broken
3014
+ ]
3015
+ );
3016
+ await expect(mounts).toEqual(["mount fallback"]);
3017
+ await expect(caughtErrors).toEqual(["boom"]);
3018
+ },
3019
+ "onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
3020
+ const container = setup();
3021
+ const logs = [];
3022
+ const broken = () => {
3023
+ throw new Error("boom");
3024
+ };
3025
+ app(
3026
+ container,
3027
+ {},
3028
+ () => [
3029
+ DIV,
3030
+ {
3031
+ catch: [
3032
+ ARTICLE,
3033
+ {
3034
+ onMount: (s, ele) => {
3035
+ logs.push("mount fallback");
3036
+ }
3037
+ },
3038
+ "fallback"
3039
+ ]
3040
+ },
3041
+ [
3042
+ SECTION,
3043
+ {
3044
+ onMount: (s, ele) => {
3045
+ logs.push("mount original section");
3046
+ },
3047
+ onUnmount: (s, ele) => {
3048
+ logs.push("unmount original section");
3049
+ }
3050
+ },
3051
+ broken
3052
+ ]
3053
+ ]
3054
+ );
3055
+ await expect(logs).toEqual(["mount fallback"]);
3056
+ },
2945
3057
  "onUnmount(): called when node is removed from the DOM": async () => {
2946
3058
  const container = setup();
2947
3059
  const unmounts = [];
@@ -3725,121 +3837,6 @@ var tests_mount_unmount_default = {
3725
3837
  patch({ expanded: true, showB: false });
3726
3838
  await expect(fired).toEqual(["unmount B"]);
3727
3839
  },
3728
- "onMount() + onUnmount: symmetry of calls": async () => {
3729
- const container = setup();
3730
- const state = createState({
3731
- startTime: 0,
3732
- inputReady: false,
3733
- showInput: true,
3734
- showTimer: true
3735
- });
3736
- const logs = [];
3737
- const patch = app(
3738
- container,
3739
- state,
3740
- (s) => {
3741
- return [
3742
- DIV,
3743
- s.showInput && [INPUT, {
3744
- type: "text",
3745
- placeholder: "Auto-focused on mount",
3746
- onMount: (s2, ele) => {
3747
- logs.push("Input mounted");
3748
- return { inputReady: true };
3749
- },
3750
- onUnmount: (s2, ele) => {
3751
- logs.push("Input removed");
3752
- return { inputReady: false };
3753
- }
3754
- }],
3755
- s.showTimer && [P, {
3756
- onMount: (s2, ele) => {
3757
- logs.push("Timer started");
3758
- return { startTime: Date.now() };
3759
- },
3760
- onUnmount: (s2, ele) => {
3761
- logs.push("Timer removed");
3762
- }
3763
- }, "Mount/unmount lifecycle demo"]
3764
- ];
3765
- }
3766
- );
3767
- await expect(state.inputReady).toEqual(true);
3768
- await expect(state.startTime != 0).toEqual(true);
3769
- patch({ showInput: false });
3770
- await expect(
3771
- async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
3772
- ).toSucceedAsync();
3773
- patch({ showTimer: false });
3774
- await expect(
3775
- async () => await expect(container._vode.stats.syncRenderCount >= 4).toEqual(true)
3776
- ).toSucceedAsync();
3777
- await expect(logs).toEqual([
3778
- "Input mounted",
3779
- "Timer started",
3780
- "Input removed",
3781
- "Timer removed"
3782
- ]);
3783
- },
3784
- "onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
3785
- const container = setup();
3786
- const mounts = [];
3787
- const broken = () => {
3788
- throw new Error("boom");
3789
- };
3790
- app(
3791
- container,
3792
- {},
3793
- () => [
3794
- DIV,
3795
- {
3796
- catch: [
3797
- SECTION,
3798
- {
3799
- onMount: (s, ele) => {
3800
- mounts.push("mount fallback");
3801
- }
3802
- },
3803
- "fallback"
3804
- ]
3805
- },
3806
- broken
3807
- ]
3808
- );
3809
- await expect(mounts).toEqual(["mount fallback"]);
3810
- },
3811
- "onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
3812
- const container = setup();
3813
- const mounts = [];
3814
- const caughtErrors = [];
3815
- const broken = () => {
3816
- throw new Error("boom");
3817
- };
3818
- app(
3819
- container,
3820
- {},
3821
- () => [
3822
- DIV,
3823
- {
3824
- catch: (s, err) => {
3825
- caughtErrors.push(err.message);
3826
- return [
3827
- SECTION,
3828
- {
3829
- onMount: (s2, ele) => {
3830
- mounts.push("mount fallback");
3831
- }
3832
- },
3833
- "fallback"
3834
- ];
3835
- }
3836
- },
3837
- broken
3838
- ]
3839
- );
3840
- await expect(mounts).toEqual(["mount fallback"]);
3841
- await expect(caughtErrors).toEqual(["boom"]);
3842
- },
3843
3840
  "onUnmount(): with catched component, replacement vode's onUnmount fires when removed": async () => {
3844
3841
  const container = setup();
3845
3842
  const unmounts = [];
@@ -3923,7 +3920,7 @@ var tests_mount_unmount_default = {
3923
3920
  patch({ show: false });
3924
3921
  await expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
3925
3922
  },
3926
- "onMount()/onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
3923
+ "onMount() + onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
3927
3924
  const container = setup();
3928
3925
  const logs = [];
3929
3926
  const state = createState({ show: true });
@@ -3959,43 +3956,134 @@ var tests_mount_unmount_default = {
3959
3956
  patch({ show: false });
3960
3957
  await expect(logs).toEqual(["mount article", "unmount article"]);
3961
3958
  },
3962
- "onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
3959
+ "onMount() + onUnmount: symmetry of calls": async () => {
3963
3960
  const container = setup();
3961
+ const state = createState({
3962
+ startTime: 0,
3963
+ inputReady: false,
3964
+ showInput: true,
3965
+ showTimer: true
3966
+ });
3964
3967
  const logs = [];
3965
- const broken = () => {
3966
- throw new Error("boom");
3967
- };
3968
- app(
3968
+ const patch = app(
3969
3969
  container,
3970
- {},
3971
- () => [
3970
+ state,
3971
+ (s) => {
3972
+ return [
3973
+ DIV,
3974
+ s.showInput && [INPUT, {
3975
+ type: "text",
3976
+ placeholder: "Auto-focused on mount",
3977
+ onMount: (s2, ele) => {
3978
+ logs.push("Input mounted");
3979
+ return { inputReady: true };
3980
+ },
3981
+ onUnmount: (s2, ele) => {
3982
+ logs.push("Input removed");
3983
+ return { inputReady: false };
3984
+ }
3985
+ }],
3986
+ s.showTimer && [P, {
3987
+ onMount: (s2, ele) => {
3988
+ logs.push("Timer started");
3989
+ return { startTime: Date.now() };
3990
+ },
3991
+ onUnmount: (s2, ele) => {
3992
+ logs.push("Timer removed");
3993
+ }
3994
+ }, "Mount/unmount lifecycle demo"]
3995
+ ];
3996
+ }
3997
+ );
3998
+ await expect(state.inputReady).toEqual(true);
3999
+ await expect(state.startTime != 0).toEqual(true);
4000
+ patch({ showInput: false });
4001
+ await expect(
4002
+ async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
4003
+ ).toSucceedAsync();
4004
+ patch({ showTimer: false });
4005
+ await expect(
4006
+ async () => await expect(container._vode.stats.syncRenderCount >= 4).toEqual(true)
4007
+ ).toSucceedAsync();
4008
+ await expect(logs).toEqual([
4009
+ "Input mounted",
4010
+ "Timer started",
4011
+ "Input removed",
4012
+ "Timer removed"
4013
+ ]);
4014
+ },
4015
+ "onMount() + onUnmount(): Not called when DOM does not require element creation or removal (same TAGs)": async () => {
4016
+ const container = setup();
4017
+ const logs = [];
4018
+ const Comp = (name) => () => [
4019
+ ARTICLE,
4020
+ [
3972
4021
  DIV,
3973
4022
  {
3974
- catch: [
3975
- ARTICLE,
3976
- {
3977
- onMount: (s, ele) => {
3978
- logs.push("mount fallback");
3979
- }
3980
- },
3981
- "fallback"
3982
- ]
4023
+ onMount: () => logs.push("mount " + name),
4024
+ onUnmount: () => logs.push("unmount " + name)
3983
4025
  },
4026
+ "Component " + name
4027
+ ]
4028
+ ];
4029
+ const state = createState({ showB: false, showD: false });
4030
+ app(container, state, (s) => [
4031
+ DIV,
4032
+ // this way they both "share a slot"
4033
+ s.showB ? Comp("B") : Comp("A"),
4034
+ // this way each component occupies its own "slot"
4035
+ !s.showD && Comp("C"),
4036
+ s.showD && Comp("D")
4037
+ ]);
4038
+ await expect(container).toMatch(
4039
+ [
4040
+ DIV,
3984
4041
  [
3985
- SECTION,
3986
- {
3987
- onMount: (s, ele) => {
3988
- logs.push("mount original section");
3989
- },
3990
- onUnmount: (s, ele) => {
3991
- logs.push("unmount original section");
3992
- }
3993
- },
3994
- broken
4042
+ ARTICLE,
4043
+ [DIV, "Component A"]
4044
+ ],
4045
+ [
4046
+ ARTICLE,
4047
+ [DIV, "Component C"]
3995
4048
  ]
3996
4049
  ]
3997
4050
  );
3998
- await expect(logs).toEqual(["mount fallback"]);
4051
+ await expect(logs).toEqual(["mount A", "mount C"]);
4052
+ state.patch({ showB: true });
4053
+ await expect(container).toMatch(
4054
+ [
4055
+ DIV,
4056
+ [
4057
+ ARTICLE,
4058
+ [DIV, "Component B"]
4059
+ ],
4060
+ [
4061
+ ARTICLE,
4062
+ [DIV, "Component C"]
4063
+ ]
4064
+ ]
4065
+ );
4066
+ await expect(logs).toEqual(["mount A", "mount C"]);
4067
+ state.patch({ showD: true });
4068
+ await expect(container).toMatch(
4069
+ [
4070
+ DIV,
4071
+ [
4072
+ ARTICLE,
4073
+ [DIV, "Component B"]
4074
+ ],
4075
+ [
4076
+ ARTICLE,
4077
+ [DIV, "Component D"]
4078
+ ]
4079
+ ]
4080
+ );
4081
+ await expect(logs).toEqual([
4082
+ "mount A",
4083
+ "mount C",
4084
+ "unmount C",
4085
+ "mount D"
4086
+ ]);
3999
4087
  }
4000
4088
  };
4001
4089
 
@@ -5189,8 +5277,11 @@ var tests_patch_advanced_default = {
5189
5277
  app(container, state, (s) => [DIV, s.phase, String(s.value)]);
5190
5278
  await expect(state.phase).toEqual("start");
5191
5279
  state.patch((async function* () {
5280
+ await expect(container._vode.stats.syncRenderPatchCount).toEqual(0);
5192
5281
  yield { phase: "working", value: 10 };
5282
+ await expect(container._vode.stats.syncRenderPatchCount).toEqual(1);
5193
5283
  yield { phase: "almost", value: 20 };
5284
+ await expect(container._vode.stats.syncRenderPatchCount).toEqual(2);
5194
5285
  return { phase: "done", value: 30 };
5195
5286
  })());
5196
5287
  await new Promise((r) => setTimeout(r, 0));
@@ -5223,6 +5314,87 @@ var tests_patch_advanced_default = {
5223
5314
  await delay(10);
5224
5315
  await expect(state.x).toEqual(10);
5225
5316
  await expect(state.y).toEqual(20);
5317
+ },
5318
+ "patch(): returns Promise for generator functions, can be awaited": async () => {
5319
+ const container = setup4();
5320
+ const state = createState({ count: 0 });
5321
+ app(container, state, (s) => [DIV, String(s.count)]);
5322
+ await expect(container._vode.stats.patchCount).toEqual(0);
5323
+ const result = state.patch(function* () {
5324
+ yield { count: 1 };
5325
+ return { count: 2 };
5326
+ });
5327
+ await expect(container._vode.stats.patchCount).toEqual(1);
5328
+ expect(result).toBeA("object");
5329
+ await expect(result instanceof Promise).toEqual(true);
5330
+ await result;
5331
+ await expect(container._vode.stats.patchCount).toEqual(3);
5332
+ await expect(state.count).toEqual(2);
5333
+ await expect(container).toMatch([DIV, "2"]);
5334
+ },
5335
+ "patch(): returns Promise for Promise patches, can be awaited": async () => {
5336
+ const container = setup4();
5337
+ const state = createState({ msg: "before" });
5338
+ app(container, state, (s) => [DIV, s.msg]);
5339
+ const result = state.patch(Promise.resolve({ msg: "after" }));
5340
+ expect(result).toBeA("object");
5341
+ await expect(result instanceof Promise).toEqual(true);
5342
+ await result;
5343
+ await expect(state.msg).toEqual("after");
5344
+ await expect(container).toMatch([DIV, "after"]);
5345
+ },
5346
+ "patch(): returns void for object patches": async () => {
5347
+ const container = setup4();
5348
+ const state = createState({ x: 1 });
5349
+ app(container, state, (s) => [DIV, String(s.x)]);
5350
+ const result = state.patch({ x: 2 });
5351
+ expect(result).toBeA("undefined");
5352
+ await expect(state.x).toEqual(2);
5353
+ await expect(container).toMatch([DIV, "2"]);
5354
+ },
5355
+ "patch(): forward promise error when one happens during patch": async () => {
5356
+ const container = setup4();
5357
+ const state = createState({ msg: "before" });
5358
+ app(container, state, (s) => [DIV, s.msg]);
5359
+ const mockPromise = Promise.withResolvers();
5360
+ const promisePatchResult = state.patch(mockPromise.promise);
5361
+ mockPromise.reject(new Error("promise error"));
5362
+ let err = await expect(() => promisePatchResult).toFailAsync("promise (1) error expected");
5363
+ expect(err.message).toEqual("promise error");
5364
+ err = await expect(() => state.patch(async () => {
5365
+ await delay(1);
5366
+ throw new Error("promise error");
5367
+ })).toFailAsync("promise (2) error expected");
5368
+ expect(err.message).toEqual("promise error");
5369
+ },
5370
+ "patch(): forward generator error when one happens during patch": async () => {
5371
+ const container = setup4();
5372
+ const state = createState({ msg: "before" });
5373
+ app(container, state, (s) => [DIV, s.msg]);
5374
+ const err = await expect(
5375
+ () => state.patch(
5376
+ async function* () {
5377
+ yield {};
5378
+ await delay(1);
5379
+ yield {};
5380
+ throw new Error("generator error");
5381
+ }
5382
+ )
5383
+ ).toFailAsync("generator error expected");
5384
+ expect(err.message).toEqual("generator error");
5385
+ },
5386
+ "patch(): forward error when one happens during patch": async () => {
5387
+ const container = setup4();
5388
+ const state = createState({ msg: "before" });
5389
+ app(container, state, (s) => [DIV, s.msg]);
5390
+ const err = await expect(
5391
+ () => state.patch(
5392
+ () => {
5393
+ throw new Error("void error");
5394
+ }
5395
+ )
5396
+ ).toFailAsync("void error expected");
5397
+ expect(err.message).toEqual("void error");
5226
5398
  }
5227
5399
  };
5228
5400
 
package/log.txt CHANGED
@@ -0,0 +1 @@
1
+ npm warn gitignore-fallback No .npmignore file found, using .gitignore for file exclusion. Consider creating a .npmignore file to explicitly control published files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.8.10",
3
+ "version": "1.8.11",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",