diff options
author | 2021-03-15 13:22:05 -0400 | |
---|---|---|
committer | 2021-03-15 13:22:05 -0400 | |
commit | af6b029e95e9c98e6fb9c642915d461b8d7f448e (patch) | |
tree | d70972e10884de0363e7ce4dd2a0765dafcf3dc8 /src/transform2.ts | |
download | astro-af6b029e95e9c98e6fb9c642915d461b8d7f448e.tar.gz astro-af6b029e95e9c98e6fb9c642915d461b8d7f448e.tar.zst astro-af6b029e95e9c98e6fb9c642915d461b8d7f448e.zip |
initial commit
Diffstat (limited to 'src/transform2.ts')
-rw-r--r-- | src/transform2.ts | 444 |
1 files changed, 444 insertions, 0 deletions
diff --git a/src/transform2.ts b/src/transform2.ts new file mode 100644 index 000000000..51399a48a --- /dev/null +++ b/src/transform2.ts @@ -0,0 +1,444 @@ +import type { TemplateNode } from "./@types/compiler/interfaces"; + +import path from "path"; +import astring from "astring"; +import esbuild from "esbuild"; +import eslexer from "es-module-lexer"; +import micromark from "micromark"; +import gfmSyntax from "micromark-extension-gfm"; +import matter from "gray-matter"; +// @ts-ignore +import gfmHtml from "micromark-extension-gfm/html.js"; +import { walk, parse } from "./compiler.js"; +import markdownEncode from './markdown-encode.js'; + +const { transformSync } = esbuild; + +interface Attribute { + start: 574; + end: 595; + type: "Attribute"; + name: "class"; + value: any; +} + +interface CompileOptions { + resolve: (p: string) => string +} + +const defaultCompileOptions: CompileOptions = { + resolve(p: string) { return p; } +} + +function internalImport(internalPath: string) { + return `/__hmx_internal__/${internalPath}`; +} + +function getAttributes(attrs: Attribute[]): Record<string, string> { + let result: Record<string, string> = {}; + for (const attr of attrs) { + if (attr.value === true) { + result[attr.name] = JSON.stringify(attr.value); + continue; + } + if (attr.value === false) { + continue; + } + if (attr.value.length > 1) { + result[attr.name] = + "(" + + attr.value + .map((v: TemplateNode) => { + if (v.expression) { + return v.expression; + } else { + return JSON.stringify(getTextFromAttribute(v)); + } + }) + .join("+") + + ")"; + continue; + } + const val: TemplateNode = attr.value[0]; + switch (val.type) { + case "MustacheTag": + result[attr.name] = "(" + val.expression + ")"; + continue; + case "Text": + result[attr.name] = JSON.stringify(getTextFromAttribute(val)); + continue; + default: + console.log(val); + throw new Error("UNKNOWN V"); + } + } + return result; +} + +function getTextFromAttribute(attr: any): string { + if (attr.raw !== undefined) { + return attr.raw; + } + if (attr.data !== undefined) { + return attr.data; + } + console.log(attr); + throw new Error("UNKNOWN attr"); +} + +function generateAttributes(attrs: Record<string, string>): string { + let result: string = "{"; + for (const [key, val] of Object.entries(attrs)) { + result += JSON.stringify(key) + ":" + val + ","; + } + return result + "}"; +} + +function getComponentWrapper( + _name: string, + { type, url }: { type: string; url: string }, + { resolve }: CompileOptions +) { + const [name, kind] = _name.split(":"); + switch(type) { + case '.hmx': { + if (kind) { + throw new Error(`HMX does not support :${kind}`); + } + return { + wrapper: name, + wrapperImport: ``, + }; + } + case '.jsx': { + if (kind === "dynamic") { + return { + wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify( + url.replace(/\.[^.]+$/, ".js") + )}, \`http://TEST\${import.meta.url}\`).pathname, '${resolve('preact')}')`, + wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`, + }; + } else { + return { + wrapper: `__preact_static(${name})`, + wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`, + }; + } + } + case '.svelte': { + if(kind === "dynamic") { + return { + wrapper: `__svelte_dynamic(${name}, new URL(${JSON.stringify( + url.replace(/\.[^.]+$/, ".svelte.js") + )}, \`http://TEST\${import.meta.url}\`).pathname)`, + wrapperImport: `import {__svelte_dynamic} from '${internalImport('render/svelte.js')}';`, + }; + } else { + return { + wrapper: `__svelte_static(${name})`, + wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';` + }; + } + } + case '.vue': { + if(kind === "dynamic") { + return { + wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify( + url.replace(/\.[^.]+$/, ".vue.js") + )}, \`http://TEST\${import.meta.url}\`).pathname, '${resolve('vue')}')`, + wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`, + }; + } else { + return { + wrapper: `__vue_static(${name})`, + wrapperImport: ` + import {__vue_static} from '${internalImport('render/vue.js')}'; + ` + }; + } + } + } + throw new Error("Unknown Component Type: " + name); +} + +async function convertHmxToJsx(template: string, compileOptions: CompileOptions) { + await eslexer.init; + const ast = parse(template, {}); + // Todo: Validate that `h` and `Fragment` aren't defined in the script + const script = ast.instance ? astring.generate(ast.instance.content) : ""; + const [scriptImports] = eslexer.parse(script, "optional-sourcename"); + const components = Object.fromEntries( + scriptImports.map((imp) => { + const componentType = path.posix.extname(imp.n!); + const componentName = path.posix.basename(imp.n!, componentType); + return [componentName, { type: componentType, url: imp.n! }]; + }) + ); + + const additionalImports = new Set<string>(); + let items: { name: string; jsx: string }[] = []; + let mode: "JSX" | "SCRIPT" | "SLOT" = "JSX"; + let collectionItem: { name: string; jsx: string } | undefined; + let currentItemName: string | undefined; + let currentDepth = 0; + + walk(ast.html as any, { + // @ts-ignore + enter(node: TemplateNode, parent, prop, index) { + // console.log("enter", node.type); + switch (node.type) { + case "MustacheTag": + let { code } = transformSync(node.expression, { + loader: "jsx", + jsxFactory: "h", + jsxFragment: "Fragment", + charset: "utf8", + }); + + let matches: RegExpExecArray[] = []; + let match: RegExpExecArray | null | undefined; + const H_COMPONENT_SCANNER = /h\(['"]?([A-Z].*?)['"]?,/gs; + const regex = new RegExp(H_COMPONENT_SCANNER); + while ((match = regex.exec(code))) { + matches.push(match); + } + for (const match of matches.reverse()) { + const name = match[1]; + const [componentName, componentKind] = name.split(":"); + if (!components[componentName]) { + throw new Error(`Unknown Component: ${componentName}`); + } + const { wrapper, wrapperImport } = getComponentWrapper( + name, + components[componentName], + compileOptions + ); + if (wrapperImport) { + additionalImports.add(wrapperImport); + } + if (wrapper !== name) { + code = + code.slice(0, match.index + 2) + + wrapper + + code.slice(match.index + match[0].length - 1); + } + } + collectionItem!.jsx += `,(${code.trim().replace(/\;$/, "")})`; + return; + case "Slot": + mode = "SLOT"; + collectionItem!.jsx += `,child`; + return; + case "Comment": + return; + case "Fragment": + // Ignore if its the top level fragment + // This should be cleaned up, but right now this is how the old thing worked + if (!collectionItem) { + return; + } + case "InlineComponent": + case "Element": + const name: string = node.name; + if (!name) { + console.log(node); + throw new Error("AHHHH"); + } + const attributes = getAttributes(node.attributes); + currentDepth++; + currentItemName = name; + if (!collectionItem) { + collectionItem = { name, jsx: "" }; + items.push(collectionItem); + } + collectionItem.jsx += collectionItem.jsx === "" ? "" : ","; + const COMPONENT_NAME_SCANNER = /^[A-Z]/; + if (!COMPONENT_NAME_SCANNER.test(name)) { + collectionItem.jsx += `h("${name}", ${ + attributes ? generateAttributes(attributes) : "null" + }`; + return; + } + if (name === 'Component') { + collectionItem.jsx += `h(Fragment, null`; + return; + } + const [componentName, componentKind] = name.split(":"); + const componentImportData = components[componentName]; + if (!componentImportData) { + throw new Error(`Unknown Component: ${componentName}`); + } + const { wrapper, wrapperImport } = getComponentWrapper( + name, + components[componentName], + compileOptions + ); + if (wrapperImport) { + additionalImports.add(wrapperImport); + } + collectionItem.jsx += `h(${wrapper}, ${ + attributes ? generateAttributes(attributes) : "null" + }`; + return; + case "Attribute": { + this.skip(); + return; + } + case "Text": { + const text = getTextFromAttribute(node); + if (mode === "SLOT") { + return; + } + if (!text.trim()) { + return; + } + if (!collectionItem) { + throw new Error("Not possible! TEXT:" + text); + } + if (currentItemName === "script" || currentItemName === "code") { + collectionItem.jsx += "," + JSON.stringify(text); + return; + } + collectionItem.jsx += "," + JSON.stringify(text); + return; + } + default: + console.log(node); + throw new Error("Unexpected node type: " + node.type); + } + }, + // @ts-ignore + leave(node: TemplateNode, parent, prop, index) { + // console.log("leave", node.type); + switch (node.type) { + case "Text": + case "MustacheTag": + case "Attribute": + case "Comment": + return; + case "Slot": { + const name = node.name; + if (name === "slot") { + mode = "JSX"; + } + return; + } + case "Fragment": + if (!collectionItem) { + return; + } + case "Element": + case "InlineComponent": + if (!collectionItem) { + throw new Error("Not possible! CLOSE " + node.name); + } + collectionItem.jsx += ")"; + currentDepth--; + if (currentDepth === 0) { + collectionItem = undefined; + } + return; + default: + throw new Error("Unexpected node type: " + node.type); + } + }, + }); + + /* + console.log({ + additionalImports, + script, + items, + }); + */ + + return { + script: script + "\n" + Array.from(additionalImports).join("\n"), + items, + }; +} + +async function convertMdToJsx(contents: string, compileOptions: CompileOptions) { + // This doesn't work. + const { data: _frontmatterData, content } = matter(contents); + const mdHtml = micromark(content, { + extensions: [gfmSyntax()], + htmlExtensions: [gfmHtml, markdownEncode], + }); + + const setupData = { + title: _frontmatterData.title, + description: _frontmatterData.description, + layout: _frontmatterData.layout, + content: { + frontmatter: _frontmatterData, + + // This is an awful hack due to Svelte parser disliking script tags badly. + source: content.replace(/<\/?script/g, '<SCRIPT'), + html: mdHtml, + }, + props: { + ..._frontmatterData, + }, + }; + + return convertHmxToJsx(`<script hmx="setup">export function setup() { + return ${JSON.stringify(setupData)}; + }</script><head></head><body>${mdHtml}</body>`, compileOptions); +} + +async function transformFromSource( + contents: string, + filename: string, + compileOptions: CompileOptions +): Promise<ReturnType<typeof convertHmxToJsx>> { + switch (path.extname(filename)) { + case ".hmx": + return convertHmxToJsx(contents, compileOptions); + case ".md": + return convertMdToJsx(contents, compileOptions); + default: + throw new Error("Not Supported!"); + } +} + +export async function compilePage(source: string, filename: string, opts: CompileOptions = defaultCompileOptions) { + const sourceJsx = await transformFromSource(source, filename, opts); + const headItem = sourceJsx.items.find((item) => item.name === "head"); + const bodyItem = sourceJsx.items.find((item) => item.name === "body"); + const headItemJsx = !headItem + ? "null" + : headItem.jsx.replace('"head"', 'isRoot ? "head" : Fragment'); + const bodyItemJsx = !bodyItem + ? "null" + : bodyItem.jsx.replace('"head"', 'isRoot ? "body" : Fragment'); + + const modJsx = ` +${sourceJsx.script} + +import { h, Fragment } from '${internalImport('h.js')}'; +export function head({title, description, props}, child, isRoot) { return (${headItemJsx}); } +export function body({title, description, props}, child, isRoot) { return (${bodyItemJsx}); } +`.trim(); + + return { + contents: modJsx, + }; +} + +export async function compileComponent(source: string, filename: string, opts: CompileOptions = defaultCompileOptions) { + const sourceJsx = await transformFromSource(source, filename, opts); + const componentJsx = sourceJsx.items.find( + (item) => item.name === "Component" + ); + if (!componentJsx) { + throw new Error(`${filename} <Component> expected!`); + } + const modJsx = ` + import { h, Fragment } from '${internalImport('h.js')}'; + export default function(props) { return h(Fragment, null, ${componentJsx.jsx}); } + `.trim(); + return { + contents: modJsx, + }; +} |