Skip to content

Commit

Permalink
WIP: rudimentary types for template parts
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed Jun 1, 2024
1 parent c0307b6 commit a4b1050
Show file tree
Hide file tree
Showing 22 changed files with 258 additions and 41 deletions.
3 changes: 2 additions & 1 deletion packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"license": "MIT",
"sideEffects": [],
"dependencies": {
"@typed/core": "workspace:*"
"@typed/core": "workspace:*",
"effect": "^3.2.7"
}
}
107 changes: 85 additions & 22 deletions packages/compiler/src/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,43 @@ import { Service } from "./typescript/Service.js"
export class Compiler {
private _cmdLine: ts.ParsedCommandLine
private _service: Service = new Service()
private primitives: {
string: ts.Type
number: ts.Type
boolean: ts.Type
bigint: ts.Type
null: ts.Type
undefined: ts.Type
void: ts.Type
}
readonly project: Project
checker: ts.TypeChecker

constructor(readonly directory: string, readonly tsConfig?: string) {
this._cmdLine = findTsConfig(directory, tsConfig)
this.project = this._service.openProject(this._cmdLine, this.enhanceLanguageServiceHost)
this.checker = this.project.typeChecker
this.primitives = {
string: this.checker.getStringType(),
number: this.checker.getNumberType(),
boolean: this.checker.getBooleanType(),
bigint: this.checker.getBigIntType(),
null: this.checker.getNullType(),
undefined: this.checker.getUndefinedType(),
void: this.checker.getVoidType()
}
}

parseTemplates(sourceFile: ts.SourceFile): ReadonlyArray<ParsedTemplate> {
const templates: Array<ParsedTemplate> = []

getTaggedTemplateLiteralExpressions(sourceFile).forEach((expression) => {
const tag = expression.tag.getText()
// Only parse html tagged templates
if (tag === "html") {
const literal = expression.template
const template = parseTemplateFromNode(literal)
templates.push({ literal, template })
const [template, parts] = this.parseTemplateFromNode(literal)
templates.push({ literal, template, parts })
}
})

Expand All @@ -45,6 +66,63 @@ export class Compiler {

private enhanceLanguageServiceHost = (_host: ts.LanguageServiceHost): void => {
}

private parseTemplateFromNode(node: ts.TemplateLiteral): readonly [Template, ReadonlyArray<ParsedPart>] {
if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
return [parse([node.getText().slice(1, -1)]), []]
} else {
const [head, syntaxList] = node.getChildren()
const children = syntaxList.getChildren()
const lastChild = children[children.length - 1]
const parts: Array<ParsedPart> = []
const spans = children.map((child, i) => {
if (child.kind === ts.SyntaxKind.TemplateSpan) {
const [part, literal] = child.getChildren()
parts.push(this.parsePart(part, i))
const text = literal.getText()
if (child === lastChild) return text.slice(1, -1)
return text.slice(1)
} else {
throw new Error(`Unexpected syntax kind: ${ts.SyntaxKind[child.kind]}`)
}
})

return [parse([head.getText().slice(1, -2), ...spans]), parts]
}
}

private parsePart(part: ts.Node, index: number): ParsedPart {
const type = this.project.getType(part)
return {
index,
kind: this.getPartType(type),
type
}
}

private getPartType(type: ts.Type): ParsedPart["kind"] {
if (this.isPrimitiveType(type)) return "primitive"

const properties = type.getProperties().map((p) => p.name)

let isEffect = false
let isFx = false
for (const name of properties) {
if (name.startsWith("__@DirectiveTypeId")) return "directive"
if (name.startsWith("__@EffectTypeId")) isEffect = true
if (name.startsWith("__@FxTypeId")) isFx = true
}

if (isFx && isEffect) return "fxEffect"
if (isFx) return "fx"
if (isEffect) return "effect"

return "placeholder"
}

private isPrimitiveType(type: ts.Type) {
return Object.values(this.primitives).some((t) => this.checker.isTypeAssignableTo(type, t))
}
}

// Ensure that nested templates are handled first
Expand All @@ -63,29 +141,14 @@ function getSpan(template: ParsedTemplate) {

export interface ParsedTemplate {
readonly literal: ts.TemplateLiteral
readonly parts: ReadonlyArray<ParsedPart>
readonly template: Template
}

function parseTemplateFromNode(node: ts.TemplateLiteral): Template {
if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
return parse([node.getText().slice(1, -1)])
} else {
const [head, syntaxList] = node.getChildren()
const children = syntaxList.getChildren()
const lastChild = children[children.length - 1]
const parts = children.map((child) => {
if (child.kind === ts.SyntaxKind.TemplateSpan) {
const [, literal] = child.getChildren()
const text = literal.getText()
if (child === lastChild) return text.slice(1, -1)
return text.slice(1)
} else {
throw new Error(`Unexpected syntax kind: ${ts.SyntaxKind[child.kind]}`)
}
})

return parse([head.getText().slice(1, -2), ...parts])
}
export interface ParsedPart {
readonly index: number
readonly kind: "placeholder" | "fxEffect" | "fx" | "effect" | "primitive" | "directive"
readonly type: ts.Type
}

function getTaggedTemplateLiteralExpressions(node: ts.SourceFile) {
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler/src/typescript/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class Project {
private languageService: ts.LanguageService
private program: ts.Program

readonly typeChecker: ts.TypeChecker

constructor(
documentRegistry: ts.DocumentRegistry,
diagnosticWriter: DiagnosticWriter,
Expand Down Expand Up @@ -98,6 +100,7 @@ export class Project {
documentRegistry
)
this.program = this.languageService.getProgram()!
this.typeChecker = this.program.getTypeChecker()
}

addFile(filePath: string) {
Expand All @@ -106,6 +109,14 @@ export class Project {
return this.program.getSourceFile(filePath)!
}

getType(node: ts.Node): ts.Type {
return this.typeChecker.getTypeAtLocation(node)
}

getSymbol(node: ts.Node): ts.Symbol | undefined {
return this.typeChecker.getSymbolAtLocation(node)
}

getCommandLine(): ts.ParsedCommandLine {
return this.cmdLine
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { html } from "@typed/core"

export const render = html`<div>${42n}</div>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Directive, html } from "@typed/core"

export const render = html`<div>${Directive.node((part) => part.update("Hello World"))}</div>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { html } from "@typed/core"
import { Effect } from "effect"

export const render = html`<div>${Effect.succeed(42n)}</div>`
3 changes: 3 additions & 0 deletions packages/compiler/test/fixtures/div-with-interpolated-fx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Fx, html } from "@typed/core"

export const render = html`<div>${Fx.succeed(42n)}</div>`
3 changes: 3 additions & 0 deletions packages/compiler/test/fixtures/div-with-interpolated-null.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { html } from "@typed/core"

export const render = html`<div>${null}</div>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { html } from "@typed/core"

export const render = html`<div>${42}</div>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { html, RefSubject } from "@typed/core"

const ref = RefSubject.tagged<number>()("ref")

export const render = html`<div>${ref}</div>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { html } from "@typed/core"

export const render = html`<div>${undefined}</div>`
4 changes: 4 additions & 0 deletions packages/compiler/test/fixtures/div-with-interpolated-void.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { html } from "@typed/core"

const value: void = void 0
export const render = html`<div>${value}</div>`
108 changes: 108 additions & 0 deletions packages/compiler/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("Compiler", () => {
const expected = new Template([new ElementNode("div", [], [new TextNode("Hello World")])], ``, [])

equalTemplates(div.template, expected)
expect(div.parts).toEqual([])
})

it("<div> with interpolated text", () => {
Expand All @@ -43,6 +44,106 @@ describe("Compiler", () => {
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated bigint", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-bigint.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated null", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-null.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated number", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-number.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated undefined", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-undefined.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated void", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-void.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "primitive" })
})

it("<div> with interpolated effect", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-effect.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "effect" })
})

it("<div> with interpolated fx", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-fx.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "fx" })
})

it("<div> with interpolated RefSubject", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-refsubject.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "fxEffect" })
})

it("<div> with interpolated Directive", () => {
const templates = compiler.parseTemplates(files["div-with-interpolated-directive.ts"])
expect(templates).toHaveLength(1)
const [div] = templates
const nodePart = new NodePart(0)
const expected = new Template([new ElementNode("div", [], [nodePart])], ``, [[nodePart, Chunk.of(0)]])

equalTemplates(div.template, expected)
equalParts(div.parts, { index: 0, kind: "directive" })
})

it("nested template", () => {
Expand All @@ -64,3 +165,10 @@ function equalTemplates(actual: Template, expected: Template) {
expect(actual.nodes).toEqual(expected.nodes)
expect(actual.parts).toEqual(expected.parts)
}

function equalParts(actual: ReadonlyArray<_.ParsedPart>, ...expected: ReadonlyArray<Omit<_.ParsedPart, "type">>) {
actual.forEach((p, i) => {
expect(p.index).toEqual(expected[i].index)
expect(p.kind).toEqual(expected[i].kind)
})
}
4 changes: 2 additions & 2 deletions packages/fx/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { FxEffectBase } from "./internal/protos.js"
import { hold } from "./internal/share.js"
import * as RefSubject from "./RefSubject.js"
import type * as Sink from "./Sink.js"
import { RefSubjectTypeId, TypeId } from "./TypeId.js"
import { FxTypeId, RefSubjectTypeId } from "./TypeId.js"
import type * as Versioned from "./Versioned.js"

/**
Expand Down Expand Up @@ -484,6 +484,6 @@ const propOf = <O, E, R>(
| Effect.Effect<O, E, R>,
key: keyof O
) => {
if (TypeId in input) return core.map(input, (o) => o[key])
if (FxTypeId in input) return core.map(input, (o) => o[key])
else return Effect.map(input, (o) => o[key])
}
6 changes: 3 additions & 3 deletions packages/fx/src/Fx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ import * as coreWithKey from "./internal/withKey.js"
import { type RefSubject, transform } from "./RefSubject.js"
import * as Sink from "./Sink.js"
import type * as Subject from "./Subject.js"
import { TypeId } from "./TypeId.js"
import { FxTypeId } from "./TypeId.js"

/**
* Fx is a push-based reactive primitive built atop of Effect.
* @since 1.20.0
*/
export interface Fx<out A, out E = never, out R = never> extends Pipeable.Pipeable {
readonly [TypeId]: Fx.Variance<A, E, R>
readonly [FxTypeId]: Fx.Variance<A, E, R>

/**
* @since 1.20.0
Expand Down Expand Up @@ -119,7 +119,7 @@ export const unify = <T extends Fx<any, any, any>>(fx: T): Unify<T> => fx as any
* @since 1.20.0
*/
export function isFx<A, E, R>(u: unknown): u is Fx<A, E, R> {
return u === null ? false : hasProperty(u, TypeId)
return u === null ? false : hasProperty(u, FxTypeId)
}

/**
Expand Down
Loading

0 comments on commit a4b1050

Please sign in to comment.