@typed/template 0.3.7 → 0.3.8

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 (55) hide show
  1. package/dist/cjs/Many.js +22 -2
  2. package/dist/cjs/Many.js.map +1 -1
  3. package/dist/cjs/Meta.js +7 -1
  4. package/dist/cjs/Meta.js.map +1 -1
  5. package/dist/cjs/Test.js +53 -0
  6. package/dist/cjs/Test.js.map +1 -1
  7. package/dist/cjs/internal/HydrateContext.js.map +1 -1
  8. package/dist/cjs/internal/errors.js +10 -2
  9. package/dist/cjs/internal/errors.js.map +1 -1
  10. package/dist/cjs/internal/hydrate.js +44 -10
  11. package/dist/cjs/internal/hydrate.js.map +1 -1
  12. package/dist/cjs/internal/parser.js +1 -1
  13. package/dist/cjs/internal/parser.js.map +1 -1
  14. package/dist/cjs/internal/render.js.map +1 -1
  15. package/dist/cjs/internal/utils.js +4 -0
  16. package/dist/cjs/internal/utils.js.map +1 -1
  17. package/dist/dts/Many.d.ts.map +1 -1
  18. package/dist/dts/Meta.d.ts +5 -0
  19. package/dist/dts/Meta.d.ts.map +1 -1
  20. package/dist/dts/Test.d.ts +17 -0
  21. package/dist/dts/Test.d.ts.map +1 -1
  22. package/dist/dts/internal/errors.d.ts +4 -0
  23. package/dist/dts/internal/errors.d.ts.map +1 -1
  24. package/dist/dts/internal/hydrate.d.ts +8 -5
  25. package/dist/dts/internal/hydrate.d.ts.map +1 -1
  26. package/dist/dts/internal/parser.d.ts.map +1 -1
  27. package/dist/dts/internal/render.d.ts.map +1 -1
  28. package/dist/dts/internal/utils.d.ts +1 -0
  29. package/dist/dts/internal/utils.d.ts.map +1 -1
  30. package/dist/esm/Many.js +19 -2
  31. package/dist/esm/Many.js.map +1 -1
  32. package/dist/esm/Meta.js +5 -0
  33. package/dist/esm/Meta.js.map +1 -1
  34. package/dist/esm/Test.js +49 -1
  35. package/dist/esm/Test.js.map +1 -1
  36. package/dist/esm/internal/HydrateContext.js.map +1 -1
  37. package/dist/esm/internal/errors.js +9 -1
  38. package/dist/esm/internal/errors.js.map +1 -1
  39. package/dist/esm/internal/hydrate.js +42 -13
  40. package/dist/esm/internal/hydrate.js.map +1 -1
  41. package/dist/esm/internal/parser.js +1 -1
  42. package/dist/esm/internal/parser.js.map +1 -1
  43. package/dist/esm/internal/render.js.map +1 -1
  44. package/dist/esm/internal/utils.js +3 -0
  45. package/dist/esm/internal/utils.js.map +1 -1
  46. package/package.json +10 -10
  47. package/src/Many.ts +23 -4
  48. package/src/Meta.ts +6 -0
  49. package/src/Test.ts +90 -1
  50. package/src/internal/HydrateContext.ts +3 -1
  51. package/src/internal/errors.ts +8 -1
  52. package/src/internal/hydrate.ts +60 -15
  53. package/src/internal/parser.ts +1 -1
  54. package/src/internal/render.ts +0 -2
  55. package/src/internal/utils.ts +4 -0
package/src/Test.ts CHANGED
@@ -10,6 +10,7 @@ import * as Fx from "@typed/fx/Fx"
10
10
  import * as RefArray from "@typed/fx/RefArray"
11
11
  import * as RefSubject from "@typed/fx/RefSubject"
12
12
  import * as Sink from "@typed/fx/Sink"
13
+ import { type Rendered } from "@typed/wire"
13
14
  import * as Cause from "effect/Cause"
14
15
  import * as Effect from "effect/Effect"
15
16
  import * as Either from "effect/Either"
@@ -17,7 +18,9 @@ import * as Fiber from "effect/Fiber"
17
18
  import type * as Scope from "effect/Scope"
18
19
  import * as ElementRef from "./ElementRef.js"
19
20
  import { ROOT_CSS_SELECTOR } from "./ElementSource.js"
20
- import { adjustTime } from "./internal/utils.js"
21
+ import { renderToHtmlString } from "./Html.js"
22
+ import { hydrate } from "./Hydrate.js"
23
+ import { adjustTime, isCommentWithValue } from "./internal/utils.js"
21
24
  import { render } from "./Render.js"
22
25
  import * as RenderContext from "./RenderContext.js"
23
26
  import type { RenderEvent } from "./RenderEvent.js"
@@ -172,3 +175,89 @@ function getOrMakeWindow(
172
175
  import("happy-dom").then((happyDOM) => new happyDOM.Window(options) as any as Window & GlobalThis)
173
176
  )
174
177
  }
178
+
179
+ /**
180
+ * @since 1.0.0
181
+ */
182
+ export interface TestHydrate<E, Elements> extends TestRender<E> {
183
+ readonly elements: Elements
184
+ }
185
+
186
+ /**
187
+ * @since 1.0.0
188
+ */
189
+ export function testHydrate<R, E, Elements>(
190
+ fx: Fx.Fx<R, E, RenderEvent>,
191
+ f: (rendered: Rendered, window: Window & GlobalThis) => Elements,
192
+ options?:
193
+ & HappyDOMOptions
194
+ & { readonly [K in keyof DomServicesElementParams]?: (document: Document) => DomServicesElementParams[K] }
195
+ ) {
196
+ return Effect.gen(function*(_) {
197
+ const window = yield* _(getOrMakeWindow(options))
198
+ const { body } = window.document
199
+
200
+ const html = yield* _(
201
+ renderToHtmlString(fx),
202
+ Effect.provide(RenderContext.server)
203
+ )
204
+
205
+ body.innerHTML = html
206
+
207
+ const rendered = Array.from(body.childNodes)
208
+
209
+ // Remove the typed-start
210
+ if (isCommentWithValue(rendered[0], "typed-start")) {
211
+ rendered.shift()
212
+ }
213
+ // Remove the typed-end
214
+ if (isCommentWithValue(rendered[rendered.length - 1], "typed-end")) {
215
+ rendered.pop()
216
+ }
217
+
218
+ const elements = f(rendered.length === 1 ? rendered[0] : rendered, window)
219
+
220
+ const elementRef = yield* _(ElementRef.make())
221
+ const errors = yield* _(RefSubject.make<never, never, ReadonlyArray<E>>(Effect.succeed([])))
222
+ const fiber = yield* _(
223
+ fx,
224
+ hydrate,
225
+ (x) =>
226
+ x.run(Sink.make(
227
+ (cause) =>
228
+ Cause.failureOrCause(cause).pipe(
229
+ Either.match({
230
+ onLeft: (error) => RefArray.append(errors, error),
231
+ onRight: (cause) => errors.onFailure(cause)
232
+ })
233
+ ),
234
+ (rendered) => ElementRef.set(elementRef, rendered)
235
+ )),
236
+ Effect.forkScoped,
237
+ Effect.provide(RenderContext.dom(window, { skipRenderScheduling: true }))
238
+ )
239
+
240
+ const test: TestHydrate<E, Elements> = {
241
+ elements,
242
+ window,
243
+ document: window.document,
244
+ elementRef,
245
+ errors,
246
+ lastError: RefArray.last(errors),
247
+ interrupt: Fiber.interrupt(fiber),
248
+ makeEvent: (type, init) => new window.Event(type, init),
249
+ makeCustomEvent: (type, init) => new window.CustomEvent(type, init),
250
+ dispatchEvent: (options) => dispatchEvent(test, options),
251
+ click: (options) => click(test, options)
252
+ }
253
+
254
+ // Allow our fibers to start
255
+ yield* _(adjustTime(1))
256
+ yield* _(adjustTime(1))
257
+
258
+ // Await the first render
259
+ yield* _(Fx.first(elementRef), Effect.race(Effect.delay(Effect.dieMessage(`Rendering taking too long`), 1000)))
260
+
261
+ return test
262
+ })
263
+ }
@@ -10,7 +10,9 @@ export type HydrateContext = {
10
10
  readonly where: ParentChildNodes
11
11
  readonly rootIndex: number
12
12
  readonly parentTemplate: Template | null
13
- readonly childIndex?: number
13
+
14
+ // Used to match sibling components using many() to the correct elements
15
+ readonly manyIndex?: string
14
16
 
15
17
  /**@internal */
16
18
  hydrate: boolean
@@ -10,6 +10,13 @@ export class CouldNotFindRootElement extends Error {
10
10
  }
11
11
  }
12
12
 
13
+ export class CouldNotFindManyCommentError extends Error {
14
+ constructor(readonly manyIndex: string) {
15
+ super(`Could not find comment for many part ${manyIndex}`)
16
+ }
17
+ }
18
+
13
19
  export function isHydrationError(e: unknown): e is CouldNotFindCommentError | CouldNotFindRootElement {
14
- return e instanceof CouldNotFindCommentError || e instanceof CouldNotFindRootElement
20
+ return e instanceof CouldNotFindCommentError || e instanceof CouldNotFindRootElement ||
21
+ e instanceof CouldNotFindManyCommentError
15
22
  }
@@ -13,7 +13,7 @@ import { unsafeGet } from "@typed/context"
13
13
  import { Either, ExecutionStrategy, Exit } from "effect"
14
14
  import * as Scope from "effect/Scope"
15
15
  import type { Template } from "../Template.js"
16
- import { CouldNotFindCommentError, CouldNotFindRootElement } from "./errors.js"
16
+ import { CouldNotFindCommentError, CouldNotFindManyCommentError, CouldNotFindRootElement } from "./errors.js"
17
17
  import { makeEventSource } from "./EventSource.js"
18
18
  import { HydrateContext } from "./HydrateContext.js"
19
19
  import type { RenderPartContext } from "./render.js"
@@ -22,8 +22,8 @@ import {
22
22
  findPath,
23
23
  getPreviousNodes,
24
24
  isComment,
25
+ isCommentStartingWithValue,
25
26
  isCommentWithValue,
26
- isHtmlElement,
27
27
  type ParentChildNodes
28
28
  } from "./utils.js"
29
29
 
@@ -193,20 +193,36 @@ export function findTemplateResultPartChildNodes(
193
193
  parentTemplate: Template,
194
194
  childTemplate: Template,
195
195
  partIndex: number,
196
- childIndex?: number
197
- ): Either.Either<CouldNotFindRootElement | CouldNotFindCommentError, ParentChildNodes> {
196
+ manyIndex?: string
197
+ ): Either.Either<CouldNotFindRootElement | CouldNotFindManyCommentError | CouldNotFindCommentError, ParentChildNodes> {
198
198
  const [, path] = parentTemplate.parts[partIndex]
199
199
  const parentNode = findPath(where, path) as HTMLElement
200
200
  const childNodesEither = findPartChildNodes(parentNode, childTemplate.hash, partIndex)
201
201
  if (Either.isLeft(childNodesEither)) return Either.left(childNodesEither.left)
202
202
 
203
203
  const childNodes = childNodesEither.right
204
- const parentChildNodes = {
205
- parentNode,
206
- childNodes: childIndex !== undefined ? [childNodes[childIndex]] : childNodes
204
+
205
+ if (manyIndex) {
206
+ const manyChildNodes = findManyChildNodes(childNodes, manyIndex)
207
+ if (Either.isLeft(manyChildNodes)) return Either.left(manyChildNodes.left)
208
+ return Either.right<ParentChildNodes>({ parentNode, childNodes: manyChildNodes.right })
207
209
  }
208
210
 
209
- return Either.right(parentChildNodes)
211
+ return Either.right<ParentChildNodes>({
212
+ parentNode,
213
+ childNodes
214
+ })
215
+ }
216
+
217
+ export function findManyChildNodes(
218
+ childNodes: Array<Node>,
219
+ manyIndex: string
220
+ ): Either.Either<CouldNotFindManyCommentError, Array<Node>> {
221
+ const either = findManyComment(childNodes, manyIndex)
222
+ if (Either.isLeft(either)) return Either.left(either.left)
223
+
224
+ const [, index] = either.right
225
+ return Either.right(findPreviousManyComment(childNodes.slice(0, index)))
210
226
  }
211
227
 
212
228
  export function findPartChildNodes(
@@ -224,11 +240,10 @@ export function findPartChildNodes(
224
240
  for (let i = index; i > -1; --i) {
225
241
  const node = childNodes[i]
226
242
 
227
- if (isHtmlElement(node) && node.dataset.typed === hash) {
228
- nodes.unshift(node)
229
- } else if (partIndex > 0 && isCommentWithValue(node, previousHoleValue)) {
243
+ if (partIndex > 0 && isCommentWithValue(node, previousHoleValue)) {
230
244
  break
231
245
  }
246
+ nodes.unshift(node)
232
247
  }
233
248
  } else {
234
249
  return Either.right([...getPreviousNodes(comment, partIndex), comment])
@@ -260,9 +275,39 @@ export function findPartComment(
260
275
  return Either.left(new CouldNotFindCommentError(partIndex))
261
276
  }
262
277
 
278
+ export function findManyComment(
279
+ childNodes: ArrayLike<Node>,
280
+ manyIndex: string
281
+ ): Either.Either<CouldNotFindManyCommentError, readonly [Comment, number]> {
282
+ const search = `many${manyIndex}`
283
+
284
+ for (let i = 0; i < childNodes.length; ++i) {
285
+ const node = childNodes[i]
286
+
287
+ if (isCommentWithValue(node, search)) {
288
+ return Either.right([node, i] as const)
289
+ }
290
+ }
291
+
292
+ return Either.left(new CouldNotFindManyCommentError(manyIndex))
293
+ }
294
+
295
+ export function findPreviousManyComment(
296
+ childNodes: Array<Node>
297
+ ) {
298
+ for (let i = childNodes.length - 1; i > -1; --i) {
299
+ const node = childNodes[i]
300
+
301
+ if (isCommentStartingWithValue(node, "many")) {
302
+ return childNodes.slice(i + 1)
303
+ }
304
+ }
305
+ return childNodes
306
+ }
307
+
263
308
  export function getHydrateEntry({
264
- childIndex,
265
309
  document,
310
+ manyIndex,
266
311
  parentTemplate,
267
312
  renderContext,
268
313
  rootIndex,
@@ -275,15 +320,15 @@ export function getHydrateEntry({
275
320
  rootIndex: number
276
321
  parentTemplate: Template | null
277
322
  strings: TemplateStringsArray
278
- childIndex?: number
323
+ manyIndex?: string
279
324
  }): Either.Either<
280
- CouldNotFindRootElement | CouldNotFindCommentError,
325
+ CouldNotFindRootElement | CouldNotFindCommentError | CouldNotFindManyCommentError,
281
326
  { readonly template: Template; readonly wire: Node | Array<Node>; readonly where: ParentChildNodes }
282
327
  > {
283
328
  const { template } = getBrowserEntry(document, renderContext, strings)
284
329
 
285
330
  if (parentTemplate) {
286
- const either = findTemplateResultPartChildNodes(where, parentTemplate, template, rootIndex, childIndex)
331
+ const either = findTemplateResultPartChildNodes(where, parentTemplate, template, rootIndex, manyIndex)
287
332
  if (Either.isLeft(either)) {
288
333
  return Either.left(either.left)
289
334
  }
@@ -715,7 +715,7 @@ function parseTextAndParts<T>(s: string, f: (index: number) => T): Array<Templat
715
715
  return out
716
716
  }
717
717
 
718
- export const parser: Parser = globalValue(Symbol.for("@typed/template/Parser2"), () => new ParserImpl())
718
+ export const parser: Parser = globalValue(Symbol.for("@typed/template/Parser"), () => new ParserImpl())
719
719
 
720
720
  const digestSize = 2
721
721
  const multiplier = 33
@@ -43,9 +43,7 @@ import {
43
43
  import type { ParentChildNodes } from "./utils.js"
44
44
  import { findHoleComment, findPath } from "./utils.js"
45
45
 
46
- // TODO: We need to add support for hydration of templates
47
46
  // TODO: We need to re-think hydration for dynamic lists, probably just markers should be fine
48
- // TODO: We need to make Parts synchronous
49
47
 
50
48
  /**
51
49
  * @internal
@@ -14,6 +14,10 @@ export function isCommentWithValue(node: Node, value: string): node is Comment {
14
14
  return isComment(node) && node.nodeValue === value
15
15
  }
16
16
 
17
+ export function isCommentStartingWithValue(node: Node, value: string): node is Comment {
18
+ return isComment(node) && (node.nodeValue?.startsWith(value) ?? false)
19
+ }
20
+
17
21
  export function isHtmlElement(node: Node): node is HTMLElement {
18
22
  return node.nodeType === node.ELEMENT_NODE
19
23
  }