From 1aa8bbb647848710833f098e6036237f6d3b48a7 Mon Sep 17 00:00:00 2001 From: Tylor Steinberger Date: Sat, 8 Jun 2024 10:37:52 -0400 Subject: [PATCH] WIP: make hydration more compatible with compiler --- .github/workflows/main.yml | 6 +- packages/compiler/src/Compiler.ts | 48 ++++---- packages/template/src/Hydrate.ts | 1 - packages/template/src/compiler-tools.ts | 50 ++++++++ .../template/src/internal/HydrateContext.ts | 2 - packages/template/src/internal/v2/render.ts | 116 +++++++++--------- 6 files changed, 134 insertions(+), 89 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ecfc1062..0c386189c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ on: branches: [development] # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -40,6 +40,7 @@ jobs: fetch-depth: 0 - uses: ./.github/workflows/setup-repo name: Setup Repo + - run: pnpm run build - run: pnpm run test lint: @@ -54,6 +55,7 @@ jobs: fetch-depth: 0 - uses: ./.github/workflows/setup-repo name: Setup Repo + - run: pnpm run build - run: pnpm run lint docs: @@ -68,9 +70,9 @@ jobs: fetch-depth: 0 - uses: ./.github/workflows/setup-repo name: Setup Repo + - run: pnpm run build - run: pnpm run docs - publish: runs-on: ubuntu-latest strategy: diff --git a/packages/compiler/src/Compiler.ts b/packages/compiler/src/Compiler.ts index 0f323d33b..127c3e5dd 100644 --- a/packages/compiler/src/Compiler.ts +++ b/packages/compiler/src/Compiler.ts @@ -27,7 +27,7 @@ import { Service } from "./typescript/Service.js" // This whole file is a hack-and-a-half, really just prototyping features -export type CompilerTarget = "dom" | "hydrate" | "server" | "static" +export type CompilerTarget = "dom" | "server" | "static" /** * Compiler is an all-in-one cass for compile-time optimization and derivations @@ -126,8 +126,8 @@ export class Compiler { if (index > -1) { const template = templates[index] const remaining = templates.slice(index + 1) - if (target === "dom" || target === "hydrate") { - return this.replaceDom(template, remaining, importManager, target === "hydrate") + if (target === "dom") { + return this.replaceDom(template, remaining, importManager) } // return this.replaceHtml(template, remaining, target === "static") @@ -151,8 +151,7 @@ export class Compiler { private replaceDom( { parts, template }: ParsedTemplate, remaining: Array, - imports: ImportDeclarationManager, - isHydrating: boolean + imports: ImportDeclarationManager ): ts.Node { const sink = ts.factory.createParameterDeclaration([], undefined, `sink`) const ctx = new CreateNodeCtx( @@ -160,41 +159,40 @@ export class Compiler { remaining, imports, Chunk.empty(), - isHydrating ? "hydrate" : "dom", + "dom", Chunk.empty() ) const setupNodes = createDomSetupStatements(ctx) + const domNodes = Array.from(consumeNestedIterable(createDomTemplateStatements(template, ctx))) const domEffects = createDomEffectStatements(template, ctx) - // Must come last to avoid mutation affecting behaviors of other nodes above - const domNodesNestedIterable = createDomTemplateStatements(template, ctx) - imports.addImport(`@typed/fx`, "Fx") imports.addImport(`effect/Effect`, "Effect") - const domNodes: Array = Array.from(consumeNestedIterable(domNodesNestedIterable)) - return createMethodCall( "Fx", "make", [], [ - ts.factory.createArrowFunction( + ts.factory.createFunctionExpression( [], + undefined, + `render`, [], [sink], undefined, - undefined, - createMethodCall(`Effect`, `gen`, [], [ - ts.factory.createFunctionExpression( - [], - ts.factory.createToken(ts.SyntaxKind.AsteriskToken), - undefined, - [], - [ts.factory.createParameterDeclaration([], undefined, `_`)], - undefined, - ts.factory.createBlock([...setupNodes, ...domNodes, ...domEffects], true) - ) + ts.factory.createBlock([ + ts.factory.createReturnStatement(createMethodCall(`Effect`, `gen`, [], [ + ts.factory.createFunctionExpression( + [], + ts.factory.createToken(ts.SyntaxKind.AsteriskToken), + undefined, + [], + [ts.factory.createParameterDeclaration([], undefined, `_`)], + undefined, + ts.factory.createBlock([...setupNodes, ...domNodes, ...domEffects], true) + ) + ])) ]) ) ] @@ -219,6 +217,9 @@ export class Compiler { ...Array.isArray(original.after) ? original.after : original.after ? [original.after] : [], (ctx) => (sourceFile) => { const templates = this.templatesByFile.get(sourceFile.fileName) ?? this.parseTemplates(sourceFile) + + if (templates.length === 0) return sourceFile + const transformers = this.getTransformersByFileAndTarget(templates, this._compilerTarget) return transformers.reduce((file, transformer) => transformer(ctx)(file), sourceFile) } @@ -368,6 +369,7 @@ function isHtmlTag(node: ts.Node): node is ts.TaggedTemplateExpression { return false } +// Used to maintain stack-safety of recursive functions function* consumeNestedIterable( iterable: NestedIterable ): Iterable { diff --git a/packages/template/src/Hydrate.ts b/packages/template/src/Hydrate.ts index 25e0f8e9b..24d953436 100644 --- a/packages/template/src/Hydrate.ts +++ b/packages/template/src/Hydrate.ts @@ -29,7 +29,6 @@ export function hydrate( ) const ctx: HydrateContext = { where: getHydrationRoot(rootElement), - parentTemplate: null, hydrate: true } diff --git a/packages/template/src/compiler-tools.ts b/packages/template/src/compiler-tools.ts index 46bf1aa6a..67767941e 100644 --- a/packages/template/src/compiler-tools.ts +++ b/packages/template/src/compiler-tools.ts @@ -6,10 +6,12 @@ * @since 1.0.0 */ +import type { Option } from "effect" import type * as Cause from "effect/Cause" import type * as Chunk from "effect/Chunk" import type * as Effect from "effect/Effect" import type * as Scope from "effect/Scope" +import type * as internalHydrateContext from "./internal/HydrateContext.js" import * as utils from "./internal/utils.js" import * as hydrationTemplate from "./internal/v2/hydration-template.js" import * as render from "./internal/v2/render.js" @@ -24,6 +26,41 @@ import type * as Template from "./Template.js" */ export interface TemplateContext extends render.TemplateContext {} +/** + * @since 1.0.0 + */ +export interface HydrateContext extends internalHydrateContext.HydrateContext {} + +/** + * @since 1.0.0 + */ +export interface HydrationTemplate extends hydrationTemplate.HydrationTemplate {} + +/** + * @since 1.0.0 + */ +export type HydrationNode = hydrationTemplate.HydrationNode + +/** + * @since 1.0.0 + */ +export interface HydrationHole extends hydrationTemplate.HydrationHole {} + +/** + * @since 1.0.0 + */ +export interface HydrationMany extends hydrationTemplate.HydrationMany {} + +/** + * @since 1.0.0 + */ +export interface HydrationElement extends hydrationTemplate.HydrationElement {} + +/** + * @since 1.0.0 + */ +export interface HydrationLiteral extends hydrationTemplate.HydrationLiteral {} + /** * @since 1.0.0 */ @@ -198,3 +235,16 @@ export const getChildNodes: (node: hydrationTemplate.HydrationNode) => Array) => Node = utils.findHydratePath + +/** + * @since 1.0.0 + */ +export const attemptHydration: ( + ctx: TemplateContext, + hash: string +) => Option.Option< + { + readonly where: HydrationTemplate + readonly hydrateCtx: HydrateContext + } +> = render.attemptHydration diff --git a/packages/template/src/internal/HydrateContext.ts b/packages/template/src/internal/HydrateContext.ts index 3ae0aeb25..464c5236e 100644 --- a/packages/template/src/internal/HydrateContext.ts +++ b/packages/template/src/internal/HydrateContext.ts @@ -1,5 +1,4 @@ import { Tagged } from "@typed/context" -import type { Template } from "../Template.js" import type { HydrationNode } from "./v2/hydration-template.js" /** @@ -8,7 +7,6 @@ import type { HydrationNode } from "./v2/hydration-template.js" */ export type HydrateContext = { readonly where: HydrationNode - readonly parentTemplate: Template | null // Used to match sibling components using many() to the correct elements readonly manyKey?: string diff --git a/packages/template/src/internal/v2/render.ts b/packages/template/src/internal/v2/render.ts index ca194a8b0..f93e92582 100644 --- a/packages/template/src/internal/v2/render.ts +++ b/packages/template/src/internal/v2/render.ts @@ -12,7 +12,6 @@ import * as Scope from "effect/Scope" import { type Directive, isDirective } from "../../Directive.js" import * as ElementRef from "../../ElementRef.js" import * as ElementSource from "../../ElementSource.js" -import type { BrowserEntry } from "../../Entry.js" import * as EventHandler from "../../EventHandler.js" import type { Placeholder } from "../../Placeholder.js" import type { ToRendered } from "../../Render.js" @@ -97,7 +96,44 @@ export const renderTemplate: ( sink.onFailure ) - const [wire, isHydrating] = yield* setupTemplate(entry, ctx) + const hydration = attemptHydration(ctx, entry.template.hash) + + let effects: Array> + let content: DocumentFragment + let wire: Rendered | undefined + + if (Option.isSome(hydration)) { + const { hydrateCtx, where } = hydration.value + effects = setupHydrateParts(entry.template.parts, { + ...ctx, + where, + manyKey: hydrateCtx.manyKey, + makeHydrateContext: (where: HydrationNode): HydrateContext => ({ + where, + hydrate: true + }) + }) + + wire = getWire(where) + } else { + content = ctx.document.importNode(entry.content, true) + effects = setupRenderParts(entry.template.parts, content, ctx) + } + + if (effects.length > 0) { + yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope))) + } + + // If there's anything to wait on and it's not already done, wait for an initial value + // for all asynchronous sources. + if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) { + yield* ctx.refCounter.wait + } + + // If we're not hydrating, we need to create our wire from our content + if (wire === undefined) { + wire = persistent(ctx.document, content!) + } // Setup our event listeners for our wire. // We use the parentScope to allow event listeners to exist @@ -105,8 +141,8 @@ export const renderTemplate: ( yield* ctx.eventSource.setup(wire, ctx.parentScope) // If we're hydrating, we need to mark this part of the stack as hydrated - if (isHydrating && Option.isSome(ctx.hydrateContext)) { - ctx.hydrateContext.value.hydrate = false + if (Option.isSome(hydration)) { + hydration.value.hydrateCtx.hydrate = false } // Emit our DomRenderEvent @@ -168,63 +204,22 @@ export function makeTemplateContext ({ - where, - parentTemplate: entry.template, - hydrate: true - }) - - const effects = setupHydrateParts(entry.template.parts, { - ...ctx, - where, - manyKey: hydrateCtx.manyKey, - makeHydrateContext - }) - if (effects.length > 0) { - yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope))) - } - - // If there's anything to wait on and it's not already done, wait for an initial value - // for all asynchronous sources. - if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) { - yield* ctx.refCounter.wait - } - - return [wire, true] as const - } - } - - // Standard Render - - const content = ctx.document.importNode(entry.content, true) - - // Setup all parts - const effects = setupRenderParts(entry.template.parts, content, ctx) - if (effects.length > 0) { - yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope))) - } - - // If there's anything to wait on and it's not already done, wait for an initial value - // for all asynchronous sources. - if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) { - yield* ctx.refCounter.wait +export function attemptHydration( + ctx: TemplateContext, + hash: string +): Option.Option<{ readonly where: HydrationTemplate; readonly hydrateCtx: HydrateContext }> { + if (Option.isSome(ctx.hydrateContext) && ctx.hydrateContext.value.hydrate) { + const hydrateCtx = ctx.hydrateContext.value + const where = findHydrationTemplateByHash(hydrateCtx, hash) + if (where === null) { + hydrateCtx.hydrate = false + return Option.none() + } else { + return Option.some({ where, hydrateCtx }) } + } - // Create a persistent wire from our content - return [persistent(ctx.document, content), false] as const - }) + return Option.none() } function setupRenderParts( @@ -576,7 +571,6 @@ function unwrapRenderable(renderable: unknown): Fx.Fx { else if (Array.isArray(renderable)) { return renderable.length === 0 ? Fx.succeed(null) - // TODO: We need to ensure the ordering of these values in server environments : Fx.map(Fx.tuple(renderable.map(unwrapRenderable)), (xs) => xs.flat()) as any } else if (Fx.FxTypeId in renderable) { return renderable as any @@ -669,7 +663,7 @@ export type HydrateTemplateContext = TemplateContext & { readonly makeHydrateContext: (where: HydrationNode, index: number) => HydrateContext } -export function findWhere(hydrateCtx: HydrateContext, hash: string): HydrationTemplate | null { +export function findHydrationTemplateByHash(hydrateCtx: HydrateContext, hash: string): HydrationTemplate | null { // If there is not a manyKey, we can just find the template by its hash if (hydrateCtx.manyKey === undefined) { return findHydrationTemplate(getChildNodes(hydrateCtx.where), hash)