From a2881bf8b3814601a6e479659005665df1894e6a Mon Sep 17 00:00:00 2001 From: Tylor Steinberger Date: Sun, 2 Jun 2024 22:48:44 -0400 Subject: [PATCH] WIP: compiling nested templates --- packages/compiler/src/Compiler.ts | 465 +++++++++++++++++- packages/compiler/src/typescript/Project.ts | 89 +--- packages/compiler/src/typescript/Service.ts | 6 +- packages/compiler/src/typescript/factories.ts | 62 ++- .../div-with-interpolated-refsubject.ts | 2 +- packages/compiler/test/index.ts | 283 ++++++----- 6 files changed, 681 insertions(+), 226 deletions(-) diff --git a/packages/compiler/src/Compiler.ts b/packages/compiler/src/Compiler.ts index 8467339ba..9eac70220 100644 --- a/packages/compiler/src/Compiler.ts +++ b/packages/compiler/src/Compiler.ts @@ -6,12 +6,27 @@ */ import { parse } from "@typed/template/Parser" -import type { Template } from "@typed/template/Template" +import type * as Template from "@typed/template/Template" +import { Chunk } from "effect" import ts from "typescript" +import { + appendChild, + createConst, + createEffectYield, + createElement, + createFunctionCall, + createMethodCall, + createTypeReference, + createUnion, + setAttribute, + toggleAttribute +} from "./typescript/factories.js" import { findTsConfig } from "./typescript/findConfigFile.js" import type { Project } from "./typescript/Project.js" import { Service } from "./typescript/Service.js" +// This whole file is a hack-and-a-half, really just prototyping features + /** * Compiler is an all-in-one cass for compile-time optimization and derivations * of Typed libraries and applications. @@ -30,10 +45,16 @@ export class Compiler { undefined: ts.Type void: ts.Type } + private templatesByFile: Map> = new Map() + readonly project: Project - checker: ts.TypeChecker + readonly checker: ts.TypeChecker - constructor(readonly directory: string, readonly tsConfig?: string) { + constructor( + readonly directory: string, + readonly tsConfig?: string, + readonly target: "dom" | "server" | "static" = "dom" + ) { this._cmdLine = findTsConfig(directory, tsConfig) this.project = this._service.openProject(this._cmdLine, this.enhanceLanguageServiceHost) this.checker = this.project.typeChecker @@ -57,13 +78,146 @@ export class Compiler { templates.push({ literal, template, parts }) }) - return templates.sort(sortParsedTemplates) + templates.sort(sortParsedTemplates) + + this.templatesByFile.set(sourceFile.fileName, templates) + + return templates + } + + compileTemplates( + sourceFile: ts.SourceFile + ) { + const files = this.project.emitFile(sourceFile.fileName) + const js = ts.ScriptSnapshot.fromString(files.find((x) => x.name.endsWith(".js"))!.text) + const dts = ts.ScriptSnapshot.fromString(files.find((x) => x.name.endsWith(".d.ts"))!.text) + const map = ts.ScriptSnapshot.fromString(files.find((x) => x.name.endsWith(".js.map"))!.text) + + return { + js, + dts, + map + } + } + + private getTransformersByFileAndTarget( + templates: ReadonlyArray + ): Array> { + return [ + (ctx) => (sourceFile) => { + const templateVisitor = (node: ts.Node): ts.Node => { + if (isHtmlTag(node)) { + const [, literal] = node.getChildren() + const index = templates.findIndex((t) => + t.literal.getStart() === literal.getStart() && t.literal.getEnd() === literal.getEnd() + ) + + if (index > -1) { + const template = templates[index] + const remaining = templates.slice(index + 1) + if (this.target === "dom") { + return this.replaceDom(template, remaining) + } + + // return this.replaceHtml(template, remaining, target === "static") + } + } + + return ts.visitEachChild(node, templateVisitor, ctx) + } + + const file = ts.visitEachChild(sourceFile, templateVisitor, ctx) + + // TODO: Add our imports + + return file + } + ] } - private enhanceLanguageServiceHost = (_host: ts.LanguageServiceHost): void => { + private replaceDom( + { parts, template }: ParsedTemplate, + remaining: ReadonlyArray + ): ts.Node { + // TODO: Generate our function block + // - Generate all of our base elements + // - Track the path of each element + // - Connect optimized variants of interpolations + // TOOD: Generate Import types + + const outputType = createTypeReference(`RenderEvent`) + const errorType = createUnion(parts.flatMap((p) => partToErrorTypes(this.checker, p))) + const contextType = createTypeReference(`SinkContext`) + const sink = ts.factory.createParameterDeclaration( + [], + undefined, + `sink`, + undefined, + createTypeReference(`Sink`, outputType, errorType, contextType) + ) + + const domNodes = createDomTemplateStatements( + template, + new CreateNodeCtx(parts, remaining, Chunk.empty(), this.target, { value: -1 }) + ) + const domEffects = createDomEffectStatements(template) + + return createMethodCall( + "Fx", + "make", + [], + [ + ts.factory.createArrowFunction( + [], + [ts.factory.createTypeParameterDeclaration([], `SinkContext`)], + [sink], + undefined, + undefined, + createMethodCall(`Effect`, `gen`, [], [ + ts.factory.createFunctionExpression( + [], + ts.factory.createToken(ts.SyntaxKind.AsteriskToken), + undefined, + [], + [ts.factory.createParameterDeclaration([], undefined, `_`)], + undefined, + ts.factory.createBlock([...domNodes, ...domEffects], true) + ) + ]) + ) + ] + ) + } + + // private replaceHtml( + // template: ParsedTemplate, + // remaining: ReadonlyArray, + // isStatic: boolean + // ) { + // return node + // } + + private enhanceLanguageServiceHost = (host: ts.LanguageServiceHost): void => { + const originalGetCustomTransformers = host.getCustomTransformers + host.getCustomTransformers = () => { + const original = originalGetCustomTransformers?.() ?? {} + return { + ...original, + after: [ + ...Array.isArray(original.after) ? original.after : original.after ? [original.after] : [], + (ctx) => (sourceFile) => { + const templates = this.templatesByFile.get(sourceFile.fileName) ?? this.parseTemplates(sourceFile) + const transformers = this.getTransformersByFileAndTarget(templates) + return transformers.reduce((file, transformer) => transformer(ctx)(file), sourceFile) + } + ] + } + } + + // TODO: Enable virtual modules } - private parseTemplateFromNode(node: ts.TemplateLiteral): readonly [Template, ReadonlyArray] { + private parseTemplateFromNode(node: ts.TemplateLiteral): readonly [Template.Template, ReadonlyArray] { if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { return [parse([node.getText().slice(1, -1)]), []] } else { @@ -126,8 +280,8 @@ export class Compiler { function sortParsedTemplates(a: ParsedTemplate, b: ParsedTemplate): number { const [, aEnd] = getSpan(a) const [bStart] = getSpan(b) - if (bStart < aEnd) return -1 - if (aEnd < bStart) return 1 + if (bStart > aEnd) return -1 + if (aEnd > bStart) return 1 return 0 } @@ -139,7 +293,7 @@ function getSpan(template: ParsedTemplate) { export interface ParsedTemplate { readonly literal: ts.TemplateLiteral readonly parts: ReadonlyArray - readonly template: Template + readonly template: Template.Template } export interface ParsedPart { @@ -148,8 +302,8 @@ export interface ParsedPart { readonly type: ts.Type } -function getHtmlTags(node: ts.SourceFile) { - const toProcess: Array = node.getChildren() +function getHtmlTags(sourceFile: ts.SourceFile) { + const toProcess: Array = sourceFile.getChildren(sourceFile) const matches: Array = [] while (toProcess.length) { @@ -158,7 +312,7 @@ function getHtmlTags(node: ts.SourceFile) { matches.push(node) } - toProcess.push(...node.getChildren()) + toProcess.push(...node.getChildren(sourceFile)) } return matches @@ -172,3 +326,290 @@ function isHtmlTag(node: ts.Node): node is ts.TaggedTemplateExpression { return false } + +function partToErrorTypes(checker: ts.TypeChecker, part: ParsedPart): Array { + if (part.kind === "primitive") return [] + const [, errorType] = getTypeArguments(checker, part.type) + return errorType ? [errorType] : [] +} + +// function partToContextTypes(checker: ts.TypeChecker, part: ParsedPart): Array { +// if (part.kind === "primitive") return [] +// const [, , contextType] = getTypeArguments(checker, part.type) +// return contextType ? [contextType] : [] +// } + +function getTypeArguments(checker: ts.TypeChecker, type: ts.Type): Array { + return checker.getTypeArguments(type as ts.TypeReference).map((t) => checker.typeToTypeNode(t, undefined, undefined)!) +} + +function createDomTemplateStatements( + template: Template.Template, + currentCtx: CreateNodeCtx +): Array { + return template.nodes.flatMap((node, i) => + createNodeStatements(node, { ...currentCtx, path: Chunk.append(currentCtx.path, i) }) + ) +} + +function createDomEffectStatements(template: Template.Template) { + const effects: Array = [] + const statements: Array = [] + + // If there's more than one element, we need to wire them together + if (template.nodes.length > 1) { + statements.push( + createConst( + `wire`, + ts.factory.createCallExpression( + ts.factory.createIdentifier(`persistent`), + [], + [ts.factory.createArrayLiteralExpression( + template.nodes.map((_, i) => ts.factory.createIdentifier(`element${i}`)) + )] + ) + ) + ) + } + + // Emit our DOM effects + effects.push( + ts.factory.createExpressionStatement( + createEffectYield( + createMethodCall("sink", "onSuccess", [], [ + createFunctionCall(`DomRenderEvent`, [ + ts.factory.createIdentifier(template.nodes.length > 1 ? "wire" : "element0") + ]) + ]) + ) + ) + ) + + // Allow the template to last forever + effects.push( + ts.factory.createExpressionStatement( + createEffectYield(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(`Effect`), `never`)) + ) + ) + + return [...statements, ...effects] +} + +class CreateNodeCtx { + constructor( + readonly parts: ReadonlyArray, + readonly remaining: ReadonlyArray, + readonly path: Chunk.Chunk, + readonly target: "dom" | "server" | "static", + readonly templateIndex: { value: number } + ) {} +} + +function createNodeStatements(node: Template.Node, ctx: CreateNodeCtx): Array { + switch (node._tag) { + case "element": + return createElementStatements(node, ctx) + case "node": + return createNodePartStatements(node, ctx) + default: + return [] + } +} + +function createElementStatements(node: Template.ElementNode, ctx: CreateNodeCtx): Array { + const statements: Array = [] + const varName = createNodeVarName(node, ctx) + + // Create the elements + statements.push(createConst(varName, createElement(node.tagName))) + // Setup any attributes + statements.push(...node.attributes.flatMap((attr) => createAttributeStatements(varName, attr))) + // Create the children + statements.push( + ...node.children.flatMap((child) => createNodeStatements(child, ctx)) + ) + + return statements +} + +function createNodeVarName(node: Pick, ctx: CreateNodeCtx): string { + const base = `${node._tag.replace("-", "_")}${Array.from(ctx.path).join("")}` + if (ctx.templateIndex.value === -1) return base + return `template${ctx.templateIndex.value}_${base}` +} + +// Attributes + +function createAttributeStatements(parent: string, attr: Template.Attribute): Array { + switch (attr._tag) { + case "attribute": + return createAttributeNodeStatements(parent, attr) + case "attr": + return createAttributePartStatements(parent, attr) + case "boolean": + return createBooleanNodeStatements(parent, attr) + case "boolean-part": + return createBooleanPartStatements(parent, attr) + case "className-part": + return createClassNamePartStatements(parent, attr) + case "data": + return createDataNodeStatements(parent, attr) + case "event": + return createEventNodeStatements(parent, attr) + case "properties": + return createPropertiesNodeStatements(parent, attr) + case "property": + return createPropertyNodeStatements(parent, attr) + case "ref": + return createRefNodeStatements(parent, attr) + case "sparse-attr": + return createSparseAttrNodeStatements(parent, attr) + case "sparse-class-name": + return createSparseClassNameNodeStatements(parent, attr) + case "text": + return createTextNodeStatements(parent, attr) + } +} + +function createAttributeNodeStatements(parent: string, attr: Template.AttributeNode): Array { + return [ + ts.factory.createExpressionStatement(setAttribute(parent, attr.name, attr.value)) + ] +} + +function createAttributePartStatements(_parent: string, _attr: Template.AttrPartNode): Array { + return [] +} + +function createBooleanNodeStatements(parent: string, attr: Template.BooleanNode): Array { + return [ + ts.factory.createExpressionStatement(toggleAttribute(parent, attr.name)) + ] +} + +function createBooleanPartStatements(_parent: string, _attr: Template.BooleanPartNode): Array { + return [] +} + +function createClassNamePartStatements(_parent: string, _attr: Template.ClassNamePartNode): Array { + return [] +} + +function createDataNodeStatements(_parent: string, _attr: Template.DataPartNode): Array { + return [] +} + +function createEventNodeStatements(_parent: string, _attr: Template.EventPartNode): Array { + return [] +} + +function createPropertiesNodeStatements(_parent: string, _attr: Template.PropertiesPartNode): Array { + return [] +} + +function createPropertyNodeStatements(_parent: string, _attr: Template.PropertyPartNode): Array { + return [] +} + +function createRefNodeStatements(_parent: string, _attr: Template.RefPartNode): Array { + return [] +} + +function createSparseAttrNodeStatements(_parent: string, _attr: Template.SparseAttrNode): Array { + return [] +} + +function createSparseClassNameNodeStatements( + _parent: string, + _attr: Template.SparseClassNameNode +): Array { + return [] +} + +function createTextNodeStatements(_parent: string, _attr: Template.TextNode): Array { + return [] +} + +// End of Attributes + +// Node Parts + +function createNodePartStatements(node: Template.NodePart, ctx: CreateNodeCtx): Array { + const parsedPart = ctx.parts.find((p) => p.index === node.index)! + switch (parsedPart.kind) { + case "directive": + return createDirectiveNodePartStatements(node, ctx) + case "effect": + return createEffectNodePartStatements(node, ctx) + case "fx": + return createFxNodePartStatements(node, ctx) + case "fxEffect": + return createFxEffectNodePartStatements(node, ctx) + case "placeholder": + return createPlaceholderNodePartStatements(node, ctx) + case "primitive": + return createPrimitiveNodePartStatements(node, ctx) + case "template": + return createTemplateNodePartStatements(node, ctx) + } +} + +function createDirectiveNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createEffectNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createFxNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createFxEffectNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createPlaceholderNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createPrimitiveNodePartStatements( + _node: Template.NodePart, + _ctx: CreateNodeCtx +): Array { + return [] +} + +function createTemplateNodePartStatements( + node: Template.NodePart, + currentCtx: CreateNodeCtx +): Array { + const parentElement = createNodeVarName({ _tag: "element" }, currentCtx) + currentCtx.templateIndex.value++ + const parsedTemplate = currentCtx.remaining[node.index] + const nestedCtx = { ...currentCtx, path: Chunk.empty() } + return [ + ...createDomTemplateStatements(parsedTemplate.template, nestedCtx), + ...parsedTemplate.template.nodes.map((node, i) => { + return ts.factory.createExpressionStatement( + appendChild(parentElement, createNodeVarName(node, { ...nestedCtx, path: Chunk.of(i) })) + ) + }) + ] +} diff --git a/packages/compiler/src/typescript/Project.ts b/packages/compiler/src/typescript/Project.ts index 62fe7acea..b99f70265 100644 --- a/packages/compiler/src/typescript/Project.ts +++ b/packages/compiler/src/typescript/Project.ts @@ -1,32 +1,29 @@ import ts from "typescript" import { ExternalFileCache, ProjectFileCache } from "./cache.js" -import type { DiagnosticWriter } from "./diagnostics" +import type { DiagnosticWriter } from "./diagnostics.js" export class Project { - private diagnosticWriter: DiagnosticWriter private cmdLine: ts.ParsedCommandLine - private projectFiles: ProjectFileCache - private externalFiles: ExternalFileCache - - private languageService: ts.LanguageService - private program: ts.Program - + readonly projectFiles: ProjectFileCache + readonly externalFiles: ExternalFileCache + readonly languageService: ts.LanguageService + readonly program: ts.Program readonly typeChecker: ts.TypeChecker + readonly languageServiceHost: ts.LanguageServiceHost constructor( documentRegistry: ts.DocumentRegistry, - diagnosticWriter: DiagnosticWriter, + readonly diagnosticWriter: DiagnosticWriter, cmdLine: ts.ParsedCommandLine, enhanceLanguageServiceHost?: (host: ts.LanguageServiceHost) => void ) { - this.diagnosticWriter = diagnosticWriter this.cmdLine = cmdLine this.projectFiles = new ProjectFileCache(cmdLine.fileNames) this.externalFiles = new ExternalFileCache() - const languageServiceHost: ts.LanguageServiceHost = { + const languageServiceHost: ts.LanguageServiceHost = this.languageServiceHost = { getCompilationSettings: () => this.cmdLine.options, // getNewLine?(): string; // getProjectVersion?(): string; @@ -105,10 +102,18 @@ export class Project { addFile(filePath: string) { // Add snapshot - this.externalFiles.getSnapshot(filePath) + this.projectFiles.getSnapshot(filePath) return this.program.getSourceFile(filePath)! } + setFile(fileName: string, snapshot: ts.IScriptSnapshot): void { + this.projectFiles.set(fileName, snapshot) + } + + getSnapshot(filePath: string) { + return this.languageServiceHost.getScriptSnapshot(filePath) + } + getType(node: ts.Node): ts.Type { return this.typeChecker.getTypeAtLocation(node) } @@ -117,11 +122,7 @@ export class Project { return this.typeChecker.getSymbolAtLocation(node) } - getCommandLine(): ts.ParsedCommandLine { - return this.cmdLine - } - - private getFileDiagnostics(fileName: string): Array { + getFileDiagnostics(fileName: string): ReadonlyArray { return [ ...this.languageService.getSyntacticDiagnostics(fileName), ...this.languageService.getSemanticDiagnostics(fileName), @@ -140,62 +141,14 @@ export class Project { return true } - validate(): boolean { - // filter down the list of files to be checked - const matcher = this.cmdLine.options.checkJs ? /[.][jt]sx?$/ : /[.]tsx?$/ - const files = this.projectFiles - .getFileNames() - .filter((f) => f.match(matcher)) - - // check each file - let result = true - for (const file of files) { - // always validate the file, even if others have failed - const fileResult = this.validateFile(file) - // combine this file's result with the aggregate result - result = result && fileResult - } - return result - } - - emitFile(fileName: string): boolean { + emitFile(fileName: string): Array { const output = this.languageService.getEmitOutput(fileName) if (!output || output.emitSkipped) { this.validateFile(fileName) - return false + return [] } - output.outputFiles.forEach((o) => { - ts.sys.writeFile(o.name, o.text) - }) - return true - } - - emit(): boolean { - // emit each file - let result = true - for (const file of this.projectFiles.getFileNames()) { - // always emit the file, even if others have failed - const fileResult = this.emitFile(file) - // combine this file's result with the aggregate result - result = result && fileResult - } - return result - } - - hasFile(fileName: string): boolean { - return this.projectFiles.has(fileName) - } - - setFile(fileName: string, snapshot?: ts.IScriptSnapshot): void { - this.projectFiles.set(fileName, snapshot) - } - - removeFile(fileName: string): void { - this.projectFiles.remove(fileName) - } - removeAllFiles(): void { - this.projectFiles.removeAll() + return output.outputFiles } dispose(): void { diff --git a/packages/compiler/src/typescript/Service.ts b/packages/compiler/src/typescript/Service.ts index f60e12e2c..d3a9a2d98 100644 --- a/packages/compiler/src/typescript/Service.ts +++ b/packages/compiler/src/typescript/Service.ts @@ -1,10 +1,10 @@ import ts from "typescript" -import { createDiagnosticWriter } from "./diagnostics.js" +import { createDiagnosticWriter, type DiagnosticWriter } from "./diagnostics.js" import { Project } from "./Project.js" export class Service { - private documentRegistry - private diagnosticWriter + readonly documentRegistry: ts.DocumentRegistry + readonly diagnosticWriter: DiagnosticWriter constructor(write?: (message: string) => void) { this.documentRegistry = ts.createDocumentRegistry() diff --git a/packages/compiler/src/typescript/factories.ts b/packages/compiler/src/typescript/factories.ts index cfc443f7e..e334006d0 100644 --- a/packages/compiler/src/typescript/factories.ts +++ b/packages/compiler/src/typescript/factories.ts @@ -30,10 +30,15 @@ export function createFunctionCall(name: string, args: Array): ts * Creates a TypeScript method call * @since 1.0.0 */ -export function createMethodCall(object: string, methodName: string, args: Array): ts.CallExpression { +export function createMethodCall( + object: string, + methodName: string, + typeParams: Array, + args: Array +): ts.CallExpression { return ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(object), methodName), - undefined, + typeParams, args ) } @@ -46,6 +51,16 @@ export function createTypeReference(name: string, ...args: Array): return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(name), args) } +/** + * Creates a TypeScript union types by name + * @since 1.0.0 + */ +export function createUnion(types: Array): ts.TypeNode { + if (types.length === 0) return createTypeReference("never") + if (types.length === 1) return types[0] + return ts.factory.createUnionTypeNode(types) +} + /** * `Element` type reference * @since 1.0.0 @@ -93,7 +108,7 @@ export const SVGElementType = createTypeReference("SVGElement") * @since 1.0.0 */ export function createDocumentFragment() { - return createMethodCall("document", "createDocumentFragment", []) + return createMethodCall("document", "createDocumentFragment", [], []) } /** @@ -101,7 +116,7 @@ export function createDocumentFragment() { * @since 1.0.0 */ export function createElement(tagName: string) { - return createMethodCall("document", "createElement", [ts.factory.createStringLiteral(tagName)]) + return createMethodCall("document", "createElement", [], [ts.factory.createStringLiteral(tagName)]) } /** @@ -109,7 +124,7 @@ export function createElement(tagName: string) { * @since 1.0.0 */ export function createText(text: string) { - return createMethodCall("document", "createTextNode", [ts.factory.createStringLiteral(text)]) + return createMethodCall("document", "createTextNode", [], [ts.factory.createStringLiteral(text)]) } /** @@ -117,7 +132,7 @@ export function createText(text: string) { * @since 1.0.0 */ export function appendChild(parent: string, child: string) { - return createMethodCall(parent, "appendChild", [ts.factory.createIdentifier(child)]) + return createMethodCall(parent, "appendChild", [], [ts.factory.createIdentifier(child)]) } /** @@ -125,7 +140,7 @@ export function appendChild(parent: string, child: string) { * @since 1.0.0 */ export function removeChild(parent: string, child: string) { - return createMethodCall(parent, "removeChild", [ts.factory.createIdentifier(child)]) + return createMethodCall(parent, "removeChild", [], [ts.factory.createIdentifier(child)]) } /** @@ -133,8 +148,39 @@ export function removeChild(parent: string, child: string) { * @since 1.0.0 */ export function insertBefore(parent: string, child: string, reference?: string | null) { - return createMethodCall(parent, "insertBefore", [ + return createMethodCall(parent, "insertBefore", [], [ ts.factory.createIdentifier(child), ...(reference ? [ts.factory.createIdentifier(reference)] : []) ]) } + +export function createConst(varName: string, expression: ts.Expression): ts.Statement { + return ts.factory.createVariableStatement( + [], + ts.factory.createVariableDeclarationList( + [createVariableDeclaration(varName, undefined, expression)], + ts.NodeFlags.Const + ) + ) +} + +export function createEffectYield(expression: ts.Expression): ts.Expression { + return ts.factory.createYieldExpression( + ts.factory.createToken(ts.SyntaxKind.AsteriskToken), + ts.factory.createCallExpression(ts.factory.createIdentifier("_"), [], [expression]) + ) +} + +export function setAttribute(element: string, name: string, value: string) { + return createMethodCall(element, "setAttribute", [], [ + ts.factory.createStringLiteral(name), + ts.factory.createCallExpression(ts.factory.createIdentifier(`String`), [], [ts.factory.createStringLiteral(value)]) + ]) +} + +export function toggleAttribute(element: string, name: string) { + return createMethodCall(element, "toggleAttribute", [], [ + ts.factory.createStringLiteral(name), + ts.factory.createTrue() + ]) +} diff --git a/packages/compiler/test/fixtures/div-with-interpolated-refsubject.ts b/packages/compiler/test/fixtures/div-with-interpolated-refsubject.ts index 4a4052811..68befed1d 100644 --- a/packages/compiler/test/fixtures/div-with-interpolated-refsubject.ts +++ b/packages/compiler/test/fixtures/div-with-interpolated-refsubject.ts @@ -1,5 +1,5 @@ import { html, RefSubject } from "@typed/core" -const ref = RefSubject.tagged()("ref") +const ref = RefSubject.tagged()("ref") export const render = html`
${ref}
` diff --git a/packages/compiler/test/index.ts b/packages/compiler/test/index.ts index 883b4be2d..f332ea7c1 100644 --- a/packages/compiler/test/index.ts +++ b/packages/compiler/test/index.ts @@ -1,13 +1,15 @@ import * as _ from "@typed/compiler" import { ElementNode, NodePart, Template, TextNode } from "@typed/template/Template" import { Chunk } from "effect" -import { readdirSync } from "fs" -import path from "path" +import * as fs from "node:fs" +import * as path from "node:path" const rootDirectory = path.dirname(import.meta.dirname) const testDirectory = path.join(rootDirectory, "test") const fixturesDirectory = path.join(testDirectory, "fixtures") -const fixtures = readdirSync(fixturesDirectory).map((name) => path.join(fixturesDirectory, name)) +const fixtures = fs.readdirSync(fixturesDirectory).map((name) => path.join(fixturesDirectory, name)).filter((x) => + fs.statSync(x).isFile() +) function makeCompiler() { const compiler = new _.Compiler(rootDirectory, "tsconfig.test.json") @@ -24,141 +26,154 @@ function makeCompiler() { } describe("Compiler", () => { - const { compiler, files } = makeCompiler() - - it("Static
with text", () => { - const templates = compiler.parseTemplates(files["static-div-with-text.ts"]) - expect(templates).toHaveLength(1) - const [div] = templates - const expected = new Template([new ElementNode("div", [], [new TextNode("Hello World")])], ``, []) - - equalTemplates(div.template, expected) - expect(div.parts).toEqual([]) - }) - - it("
with interpolated text", () => { - const templates = compiler.parseTemplates(files["div-with-interpolated-text.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("
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("
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("
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("
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" }) + describe("parseTemplates", () => { + const { compiler, files } = makeCompiler() + it("Static
with text", () => { + const templates = compiler.parseTemplates(files["static-div-with-text.ts"]) + expect(templates).toHaveLength(1) + const [div] = templates + const expected = new Template([new ElementNode("div", [], [new TextNode("Hello World")])], ``, []) + + equalTemplates(div.template, expected) + expect(div.parts).toEqual([]) + }) + + it("
with interpolated text", () => { + const templates = compiler.parseTemplates(files["div-with-interpolated-text.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("
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("
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("
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("
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("
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("
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("
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("
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("
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", () => { + const templates = compiler.parseTemplates(files["nested-templates.ts"]) + + expect(templates).toHaveLength(2) + + const [div, p] = templates + const nodePart = new NodePart(0) + const expectedDiv = new Template([new ElementNode("div", [], [nodePart])], "", [[nodePart, Chunk.of(0)]]) + const expectedP = new Template([new ElementNode("p", [], [new TextNode("Hello World")])], "", []) + + equalTemplates(p.template, expectedP) + equalTemplates(div.template, expectedDiv) + equalParts(div.parts, { index: 0, kind: "template" }) + }) }) - it("
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("
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("
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("
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("
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", () => { - const templates = compiler.parseTemplates(files["nested-templates.ts"]) - - expect(templates).toHaveLength(2) + describe("compileTemplates", () => { + const { compiler, files } = makeCompiler() - const [p, div] = templates - const expectedP = new Template([new ElementNode("p", [], [new TextNode("Hello World")])], "", []) - const nodePart = new NodePart(0) - const expectedDiv = new Template([new ElementNode("div", [], [nodePart])], "", [[nodePart, Chunk.of(0)]]) + it("optimizes templates for the DOM", () => { + const nestedTemplates = compiler.compileTemplates(files["nested-templates.ts"]) + const divWithRefSubject = compiler.compileTemplates(files["div-with-interpolated-refsubject.ts"]) - equalTemplates(p.template, expectedP) - equalTemplates(div.template, expectedDiv) - equalParts(div.parts, { index: 0, kind: "template" }) + console.log(nestedTemplates.js.getText(0, nestedTemplates.js.getLength())) + console.log(divWithRefSubject.js.getText(0, divWithRefSubject.js.getLength())) + }) }) })