@typed/template 0.9.3 → 0.9.5

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.
Files changed (87) hide show
  1. package/dist/cjs/Directive.js.map +1 -1
  2. package/dist/cjs/ElementRef.js.map +1 -1
  3. package/dist/cjs/ElementSource.js.map +1 -1
  4. package/dist/cjs/Entry.js.map +1 -1
  5. package/dist/cjs/EventHandler.js.map +1 -1
  6. package/dist/cjs/Html.js +9 -6
  7. package/dist/cjs/Html.js.map +1 -1
  8. package/dist/cjs/HtmlChunk.js.map +1 -1
  9. package/dist/cjs/Hydrate.js.map +1 -1
  10. package/dist/cjs/Many.js +21 -21
  11. package/dist/cjs/Many.js.map +1 -1
  12. package/dist/cjs/Meta.js.map +1 -1
  13. package/dist/cjs/Parser.js.map +1 -1
  14. package/dist/cjs/Part.js.map +1 -1
  15. package/dist/cjs/Placeholder.js.map +1 -1
  16. package/dist/cjs/Platform.js.map +1 -1
  17. package/dist/cjs/Render.js.map +1 -1
  18. package/dist/cjs/RenderContext.js.map +1 -1
  19. package/dist/cjs/RenderEvent.js.map +1 -1
  20. package/dist/cjs/RenderTemplate.js.map +1 -1
  21. package/dist/cjs/Renderable.js.map +1 -1
  22. package/dist/cjs/Template.js +2 -1
  23. package/dist/cjs/Template.js.map +1 -1
  24. package/dist/cjs/Test.js +3 -11
  25. package/dist/cjs/Test.js.map +1 -1
  26. package/dist/cjs/Vitest.js +16 -0
  27. package/dist/cjs/Vitest.js.map +1 -1
  28. package/dist/cjs/index.js.map +1 -1
  29. package/dist/cjs/internal/EventSource.js.map +1 -1
  30. package/dist/cjs/internal/HydrateContext.js.map +1 -1
  31. package/dist/cjs/internal/browser.js.map +1 -1
  32. package/dist/cjs/internal/chunks.js.map +1 -1
  33. package/dist/cjs/internal/errors.js.map +1 -1
  34. package/dist/cjs/internal/hydrate.js +1 -1
  35. package/dist/cjs/internal/hydrate.js.map +1 -1
  36. package/dist/cjs/internal/indexRefCounter.js.map +1 -1
  37. package/dist/cjs/internal/module-augmentation.js.map +1 -1
  38. package/dist/cjs/internal/parser.js +10 -5
  39. package/dist/cjs/internal/parser.js.map +1 -1
  40. package/dist/cjs/internal/parts.js +2 -1
  41. package/dist/cjs/internal/parts.js.map +1 -1
  42. package/dist/cjs/internal/render.js +38 -17
  43. package/dist/cjs/internal/render.js.map +1 -1
  44. package/dist/cjs/internal/server.js.map +1 -1
  45. package/dist/cjs/internal/utils.js.map +1 -1
  46. package/dist/dts/Html.d.ts +7 -0
  47. package/dist/dts/Html.d.ts.map +1 -1
  48. package/dist/dts/Placeholder.d.ts +3 -3
  49. package/dist/dts/Placeholder.d.ts.map +1 -1
  50. package/dist/dts/Template.d.ts.map +1 -1
  51. package/dist/dts/Test.d.ts +3 -0
  52. package/dist/dts/Test.d.ts.map +1 -1
  53. package/dist/dts/Vitest.d.ts +12 -0
  54. package/dist/dts/Vitest.d.ts.map +1 -1
  55. package/dist/dts/internal/parser.d.ts.map +1 -1
  56. package/dist/dts/internal/parts.d.ts.map +1 -1
  57. package/dist/dts/internal/render.d.ts.map +1 -1
  58. package/dist/esm/Html.js +9 -7
  59. package/dist/esm/Html.js.map +1 -1
  60. package/dist/esm/Many.js +18 -18
  61. package/dist/esm/Many.js.map +1 -1
  62. package/dist/esm/Placeholder.js.map +1 -1
  63. package/dist/esm/Template.js +2 -1
  64. package/dist/esm/Template.js.map +1 -1
  65. package/dist/esm/Test.js +4 -12
  66. package/dist/esm/Test.js.map +1 -1
  67. package/dist/esm/Vitest.js +12 -0
  68. package/dist/esm/Vitest.js.map +1 -1
  69. package/dist/esm/internal/hydrate.js +1 -1
  70. package/dist/esm/internal/hydrate.js.map +1 -1
  71. package/dist/esm/internal/parser.js +10 -6
  72. package/dist/esm/internal/parser.js.map +1 -1
  73. package/dist/esm/internal/parts.js +2 -3
  74. package/dist/esm/internal/parts.js.map +1 -1
  75. package/dist/esm/internal/render.js +37 -17
  76. package/dist/esm/internal/render.js.map +1 -1
  77. package/package.json +9 -9
  78. package/src/Html.ts +10 -16
  79. package/src/Many.ts +26 -26
  80. package/src/Placeholder.ts +9 -3
  81. package/src/Template.ts +2 -1
  82. package/src/Test.ts +15 -18
  83. package/src/Vitest.ts +70 -0
  84. package/src/internal/hydrate.ts +1 -1
  85. package/src/internal/parser.ts +12 -6
  86. package/src/internal/parts.ts +2 -3
  87. package/src/internal/render.ts +53 -23
package/src/Test.ts CHANGED
@@ -13,6 +13,7 @@ import * as Sink from "@typed/fx/Sink"
13
13
  import { type Rendered } from "@typed/wire"
14
14
  import { Layer } from "effect"
15
15
  import * as Cause from "effect/Cause"
16
+ import type { DurationInput } from "effect/Duration"
16
17
  import * as Effect from "effect/Effect"
17
18
  import * as Either from "effect/Either"
18
19
  import * as Fiber from "effect/Fiber"
@@ -21,7 +22,7 @@ import * as ElementRef from "./ElementRef.js"
21
22
  import { ROOT_CSS_SELECTOR } from "./ElementSource.js"
22
23
  import { renderToHtmlString, serverLayer } from "./Html.js"
23
24
  import { hydrate, hydrateLayer } from "./Hydrate.js"
24
- import { adjustTime, isCommentWithValue } from "./internal/utils.js"
25
+ import { adjustTime } from "./internal/utils.js"
25
26
  import { render, renderLayer } from "./Render.js"
26
27
  import type * as RenderContext from "./RenderContext.js"
27
28
  import type { RenderEvent } from "./RenderEvent.js"
@@ -60,6 +61,7 @@ export function testRender<E, R>(
60
61
  options?:
61
62
  & HappyDOMOptions
62
63
  & { readonly [K in keyof DomServicesElementParams]?: (document: Document) => DomServicesElementParams[K] }
64
+ & { renderTimeout?: DurationInput }
63
65
  ): Effect.Effect<
64
66
  TestRender<E>,
65
67
  never,
@@ -105,7 +107,10 @@ export function testRender<E, R>(
105
107
  yield* _(adjustTime(1))
106
108
 
107
109
  // Await the first render
108
- yield* _(Fx.first(elementRef), Effect.race(Effect.delay(Effect.dieMessage(`Rendering taking too long`), 1000)))
110
+ yield* _(
111
+ Fx.first(elementRef),
112
+ Effect.race(Effect.delay(Effect.dieMessage(`Rendering taking too long`), options?.renderTimeout ?? 1000))
113
+ )
109
114
 
110
115
  return test
111
116
  })
@@ -215,18 +220,7 @@ export function testHydrate<R, E, Elements>(
215
220
  body.innerHTML = html
216
221
 
217
222
  const rendered = Array.from(body.childNodes)
218
-
219
- // Remove the typed-start
220
- if (isCommentWithValue(rendered[0], "typed-start")) {
221
- rendered.shift()
222
- }
223
- // Remove the typed-end
224
- if (isCommentWithValue(rendered[rendered.length - 1], "typed-end")) {
225
- rendered.pop()
226
- }
227
-
228
223
  const elements = f(rendered.length === 1 ? rendered[0] : rendered, window)
229
-
230
224
  const elementRef = yield* _(ElementRef.make())
231
225
  const errors = yield* _(RefSubject.make<ReadonlyArray<E>>(Effect.succeed([])))
232
226
  const fiber = yield* _(
@@ -235,11 +229,14 @@ export function testHydrate<R, E, Elements>(
235
229
  (x) =>
236
230
  x.run(Sink.make(
237
231
  (cause) =>
238
- Cause.failureOrCause(cause).pipe(
239
- Either.match({
240
- onLeft: (error) => RefArray.append(errors, error),
241
- onRight: (cause) => errors.onFailure(cause)
242
- })
232
+ Effect.zipRight(
233
+ Effect.logError(cause),
234
+ Cause.failureOrCause(cause).pipe(
235
+ Either.match({
236
+ onLeft: (error) => RefArray.append(errors, error),
237
+ onRight: (cause) => errors.onFailure(cause)
238
+ })
239
+ )
243
240
  ),
244
241
  (rendered) => ElementRef.set(elementRef, rendered)
245
242
  )),
package/src/Vitest.ts CHANGED
@@ -38,6 +38,38 @@ export function it<E, A>(
38
38
  )
39
39
  }
40
40
 
41
+ it.only = function it<E, A>(
42
+ name: string,
43
+ test: () => Effect.Effect<A, E, Scope>,
44
+ options?: vitest.TestOptions
45
+ ) {
46
+ return vitest.it.only(
47
+ name,
48
+ () =>
49
+ test().pipe(
50
+ Effect.scoped,
51
+ Effect.runPromise
52
+ ),
53
+ options
54
+ )
55
+ }
56
+
57
+ it.skip = function it<E, A>(
58
+ name: string,
59
+ test: () => Effect.Effect<A, E, Scope>,
60
+ options?: vitest.TestOptions
61
+ ) {
62
+ return vitest.it.skip(
63
+ name,
64
+ () =>
65
+ test().pipe(
66
+ Effect.scoped,
67
+ Effect.runPromise
68
+ ),
69
+ options
70
+ )
71
+ }
72
+
41
73
  /**
42
74
  * @since 1.0.0
43
75
  */
@@ -59,3 +91,41 @@ export function test<E, A>(
59
91
  options
60
92
  )
61
93
  }
94
+
95
+ test.only = function test<E, A>(
96
+ name: string,
97
+ test: (options: {
98
+ readonly clock: TestClock.TestClock
99
+ }) => Effect.Effect<A, E, Scope | TestServices.TestServices>,
100
+ options?: vitest.TestOptions
101
+ ) {
102
+ return vitest.it.only(
103
+ name,
104
+ () =>
105
+ TestClock.testClockWith((clock) => test({ clock })).pipe(
106
+ Effect.provide(TestContext.TestContext),
107
+ Effect.scoped,
108
+ Effect.runPromise
109
+ ),
110
+ options
111
+ )
112
+ }
113
+
114
+ test.skip = function test<E, A>(
115
+ name: string,
116
+ test: (options: {
117
+ readonly clock: TestClock.TestClock
118
+ }) => Effect.Effect<A, E, Scope | TestServices.TestServices>,
119
+ options?: vitest.TestOptions
120
+ ) {
121
+ return vitest.it.skip(
122
+ name,
123
+ () =>
124
+ TestClock.testClockWith((clock) => test({ clock })).pipe(
125
+ Effect.provide(TestContext.TestContext),
126
+ Effect.scoped,
127
+ Effect.runPromise
128
+ ),
129
+ options
130
+ )
131
+ }
@@ -161,7 +161,7 @@ export function findRootChildNodes(where: HTMLElement): Array<Node> {
161
161
  }
162
162
  }
163
163
 
164
- for (let i = length - 1; i >= start; i--) {
164
+ for (let i = length - 1; i >= Math.max(start, 0); i--) {
165
165
  const node = childNodes[i]
166
166
 
167
167
  if (node.nodeType === node.COMMENT_NODE && node.nodeValue === END) {
@@ -287,10 +287,11 @@ class ParserImpl implements Parser {
287
287
  this._skipWhitespace = false
288
288
  return [this.addPartWithPrevious(new Template.NodePart(parseInt(next.match[2], 10)))]
289
289
  } else if ((next = this.chunk(getWhitespace))) { // Whitespace
290
- return this._skipWhitespace ? [] : [new Template.TextNode(next.match[1])]
290
+ return this._skipWhitespace ? [] : (this.path.inc(), [new Template.TextNode(next.match[1])])
291
291
  } else if ((next = this.chunk(getTextUntilCloseBrace))) { // Text and parts
292
292
  return parseTextAndParts(next.match[1], (i) => this.addPartWithPrevious(new Template.NodePart(i)))
293
293
  } else {
294
+ this.path.inc()
294
295
  return [new Template.TextNode(this.nextChar())]
295
296
  }
296
297
  }
@@ -346,7 +347,7 @@ class ParserImpl implements Parser {
346
347
  const attributes = this.parseAttributes()
347
348
 
348
349
  this.path.push()
349
- const children = this.parseTextChildren()
350
+ const children = this.parseTextChildren(tagName)
350
351
  this.path.pop()
351
352
 
352
353
  return new Template.TextOnlyElement(tagName, attributes, children || [])
@@ -516,11 +517,11 @@ class ParserImpl implements Parser {
516
517
  }
517
518
  }
518
519
 
519
- private parseTextChildren(): Array<Template.Text> | null {
520
- return this.parseArray(() => this.parseTextChild())
520
+ private parseTextChildren(tagName: string): Array<Template.Text> | null {
521
+ return this.parseArray(() => this.parseTextChild(tagName))
521
522
  }
522
523
 
523
- private parseTextChild(): LoopDecision<Array<Template.Text>> {
524
+ private parseTextChild(tagName: string): LoopDecision<Array<Template.Text>> {
524
525
  const [parsed, matched] = this.parseTextUntilMany(textChildMatches)
525
526
  const text = parsed.trim()
526
527
 
@@ -533,7 +534,12 @@ class ParserImpl implements Parser {
533
534
 
534
535
  return text === "" ? Continue([part]) : Continue([new Template.TextNode(text), part])
535
536
  }
536
- case "elementClose":
537
+ case "elementClose": {
538
+ this.consumeClosingTag(tagName)
539
+ return Break(
540
+ text ? [new Template.TextNode(text)] : undefined
541
+ )
542
+ }
537
543
  case "elementOpen": // In this case we make the assumption that you forgot to close this element
538
544
  return Break(
539
545
  text ? [new Template.TextNode(text)] : undefined
@@ -334,9 +334,8 @@ export class TextPartImpl extends base("text") implements TextPart {
334
334
  // TODO: Make this properly
335
335
  static browser(document: Document, index: number, element: Element, ctx: RenderContext) {
336
336
  const comment = findHoleComment(element, index)
337
- const text = comment.previousSibling && isText(comment.previousSibling)
338
- ? comment.previousSibling
339
- : document.createTextNode("")
337
+ const text = document.createTextNode("")
338
+ element.insertBefore(text, comment)
340
339
 
341
340
  return new TextPartImpl(
342
341
  index,
@@ -7,6 +7,7 @@ import { Effect, ExecutionStrategy, Exit, Runtime } from "effect"
7
7
  import type { Cause } from "effect/Cause"
8
8
  import type { Chunk } from "effect/Chunk"
9
9
  import * as Context from "effect/Context"
10
+ import { hasProperty } from "effect/Predicate"
10
11
  import * as Scope from "effect/Scope"
11
12
  import { uncapitalize } from "effect/String"
12
13
  import type { Directive } from "../Directive.js"
@@ -41,7 +42,7 @@ import {
41
42
  TextPartImpl
42
43
  } from "./parts.js"
43
44
  import type { ParentChildNodes } from "./utils.js"
44
- import { findHoleComment, findPath } from "./utils.js"
45
+ import { findPath } from "./utils.js"
45
46
 
46
47
  // TODO: We need to re-think hydration for dynamic lists, probably just markers should be fine
47
48
 
@@ -77,7 +78,7 @@ const RenderPartMap: RenderPartMap = {
77
78
  const element = node as HTMLElement | SVGElement
78
79
  const attr = createAttribute(document, element, templatePart.name)
79
80
  const renderable = values[templatePart.index]
80
- let isSet = true
81
+ let isSet = false
81
82
  const setValue = (value: string | null | undefined) => {
82
83
  if (isNullOrUndefined(value)) {
83
84
  element.removeAttribute(templatePart.name)
@@ -147,10 +148,10 @@ const RenderPartMap: RenderPartMap = {
147
148
  },
148
149
  "comment-part": (templatePart, node, ctx) => {
149
150
  const { refCounter, renderContext, values } = ctx
150
- const comment = findHoleComment(node as Element, templatePart.index)
151
+ const comment = node as Comment
151
152
  const renderable = values[templatePart.index]
152
153
  const setValue = (value: string | null | undefined) => {
153
- comment.textContent = isNullOrUndefined(value) ? "" : String(value)
154
+ comment.nodeValue = isNullOrUndefined(value) ? "" : String(value)
154
155
  }
155
156
 
156
157
  return matchSettablePart(
@@ -242,7 +243,7 @@ const RenderPartMap: RenderPartMap = {
242
243
  "property": (templatePart, node, ctx) => {
243
244
  const element = node as HTMLElement | SVGElement
244
245
  const renderable = ctx.values[templatePart.index]
245
- const setValue = (value: string | null | undefined) => {
246
+ const setValue = (value: unknown) => {
246
247
  if (isNullOrUndefined(value)) {
247
248
  delete (element as any)[templatePart.name]
248
249
  } else {
@@ -260,7 +261,6 @@ const RenderPartMap: RenderPartMap = {
260
261
  },
261
262
  "properties": (templatePart, node, ctx) => {
262
263
  const renderable = ctx.values[templatePart.index] as any as Record<string, any>
263
-
264
264
  if (isNullOrUndefined(renderable)) return null
265
265
  else if (Fx.isFx(renderable) || Effect.isEffect(renderable)) {
266
266
  throw new Error(`Properties Part must utilize an Record of renderable values.`)
@@ -407,11 +407,9 @@ const RenderPartMap: RenderPartMap = {
407
407
  const element = node as HTMLElement | SVGElement
408
408
  const attr = createAttribute(ctx.document, element, templatePart.name)
409
409
 
410
- const setValue = (value: string | null | undefined, index: number) =>
411
- Effect.suspend(() => {
412
- values[index] = value || ""
413
- return ctx.renderContext.queue.add(element, () => attr.value = values.join(""))
414
- })
410
+ const setValue = (value: string | null | undefined, index: number) => {
411
+ values[index] = value ?? ""
412
+ }
415
413
 
416
414
  const effects: Array<Effect.Effect<void, any, any>> = []
417
415
 
@@ -429,7 +427,11 @@ const RenderPartMap: RenderPartMap = {
429
427
  new AttributePartImpl(
430
428
  templatePart.name,
431
429
  node.index,
432
- ({ value }) => setValue(value, index),
430
+ ({ value }) =>
431
+ Effect.zipRight(
432
+ ctx.renderContext.queue.add(element, () => setValue(value, index)),
433
+ ctx.refCounter.release(node.index)
434
+ ),
433
435
  attr.value
434
436
  ),
435
437
  (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(node.index)),
@@ -442,6 +444,11 @@ const RenderPartMap: RenderPartMap = {
442
444
  }
443
445
  }
444
446
 
447
+ if (effects.length === 0) {
448
+ attr.value = values.join("")
449
+ element.setAttributeNode(attr)
450
+ }
451
+
445
452
  return effects
446
453
  },
447
454
  "sparse-class-name": (templatePart, node, ctx) => {
@@ -465,11 +472,12 @@ const RenderPartMap: RenderPartMap = {
465
472
  const values = Array.from({ length: templatePart.nodes.length }, (): string => "")
466
473
  const comment = node as Comment
467
474
 
468
- const setValue = (value: string | null | undefined, index: number) =>
469
- Effect.suspend(() => {
470
- values[index] = value || ""
471
- return ctx.renderContext.queue.add(comment, () => comment.textContent = values.join(""))
472
- })
475
+ const setValue = (value: string | null | undefined, index: number) => {
476
+ values[index] = value ?? ""
477
+ }
478
+ const flushValue = () => {
479
+ comment.data = values.join("")
480
+ }
473
481
 
474
482
  const effects: Array<Effect.Effect<void, any, any>> = []
475
483
 
@@ -482,11 +490,18 @@ const RenderPartMap: RenderPartMap = {
482
490
  const index = i
483
491
  const effect = matchSettablePart(
484
492
  renderable,
485
- (value) => setValue(value, index),
493
+ (value) => {
494
+ setValue(value, index)
495
+ flushValue()
496
+ },
486
497
  () =>
487
498
  new CommentPartImpl(
488
499
  node.index,
489
- ({ value }) => setValue(value, index),
500
+ ({ value }) => (setValue(value, index),
501
+ Effect.zipRight(
502
+ ctx.renderContext.queue.add(comment, () => flushValue()),
503
+ ctx.refCounter.release(node.index)
504
+ )),
490
505
  null
491
506
  ),
492
507
  (f) => Effect.zipRight(ctx.renderContext.queue.add(comment, f), ctx.refCounter.release(node.index)),
@@ -499,6 +514,10 @@ const RenderPartMap: RenderPartMap = {
499
514
  }
500
515
  }
501
516
 
517
+ if (effects.length === 0) {
518
+ flushValue()
519
+ }
520
+
502
521
  return effects
503
522
  },
504
523
  "text-part": (templatePart, node, ctx) => {
@@ -750,8 +769,6 @@ function removeChildren(where: HTMLElement, previous: Rendered) {
750
769
  }
751
770
 
752
771
  function replaceChildren(where: HTMLElement, wire: Rendered) {
753
- console.log("replaceChildren", wire)
754
-
755
772
  where.replaceChildren(...getNodes(wire))
756
773
  }
757
774
 
@@ -805,7 +822,7 @@ function buildNode(document: Document, node: Template.Node, isSvgContext: boolea
805
822
  case "comment":
806
823
  return document.createComment(node.value)
807
824
  case "sparse-comment":
808
- return document.createComment("")
825
+ return document.createComment(`hole${node.nodes.map((n) => n._tag === "text" ? "" : n.index).join("")}`)
809
826
  // Create placeholders for these elements
810
827
  case "comment-part":
811
828
  case "node":
@@ -914,7 +931,7 @@ function matchSettablePart(
914
931
  Directive: (directive) => {
915
932
  expect()
916
933
  const part = makePart()
917
- return Effect.flatMap(directive(part), () => schedule(() => setValue(part.value)))
934
+ return runDirective(directive, part, setValue, schedule)
918
935
  },
919
936
  Otherwise: (otherwise) => {
920
937
  setValue(otherwise)
@@ -939,3 +956,16 @@ function matchRenderable(renderable: Renderable<any, any>, matches: {
939
956
  return matches.Otherwise(renderable)
940
957
  }
941
958
  }
959
+
960
+ function runDirective(
961
+ directive: Directive<any, any>,
962
+ part: Part,
963
+ setValue: (value: any) => void,
964
+ schedule: (f: () => void) => Effect.Effect<void, never, Scope.Scope>
965
+ ): Effect.Effect<void, any, any> {
966
+ if (hasProperty(part, "update")) {
967
+ return directive({ ...part, update: (value: any) => schedule(() => setValue(value)) })
968
+ } else {
969
+ return Effect.flatMap(directive(part), () => schedule(() => setValue(part.value)))
970
+ }
971
+ }