Created
November 22, 2024 06:47
-
-
Save termermc/e544708b4ecdb4fac9a23d88da1ef6b4 to your computer and use it in GitHub Desktop.
Solid.js TSX component to PHP template function transpiler (proof of concept)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Note that your Vite Solid.js plugin's config must have `generate: 'ssr'` set for this to work. | |
import { writeFile } from 'node:fs/promises' | |
import { join } from 'node:path' | |
import { renderToStringAsync } from "solid-js/web" | |
import { Component, JSX } from 'solid-js' | |
import { Header } from '../src/component/layout/Header' | |
type BakeOptions = { | |
functionName: string | |
sanitizerFunc: string | |
} | |
async function bakeToString<T>(input: Component<T>, props: T, { functionName, sanitizerFunc }: BakeOptions): Promise<string> { | |
const postProcReplacements = { | |
'__PHP__OPENER__': `<?= ${sanitizerFunc}(`, | |
'__PHP__CLOSER__': ') ?>', | |
'__PHP__IF__': `<?php if (`, | |
'__PHP__THEN__': ') { ?>', | |
'__PHP__ELSE__': '<?php } else { ?>', | |
'__PHP__ENDIF__': '<?php } ?>', | |
'<!--!$-->': '', | |
'<?php if (<?= ': '<?php if (', | |
' ?>) {': ') {', | |
} | |
function processProp(value: any, baseAccessor: string, wrapInPhp: boolean): any { | |
if (typeof value !== 'string' && value.length !== undefined) { | |
const res: any[] = [] | |
for (let i = 0; i < value.length; i++) { | |
const elem = (value as any)[i] | |
res.push(processProp(elem, baseAccessor + '[' + i + ']', true)) | |
} | |
return res | |
} else if (typeof value === 'object') { | |
const res: any = {} | |
for (const [key, val] of Object.entries(value)) { | |
res[key] = processProp(val, baseAccessor + '[' + JSON.stringify(key) + ']', true) | |
} | |
return res | |
} else { | |
return wrapInPhp ? `__PHP__OPENER__${baseAccessor}__PHP__CLOSER__` : baseAccessor | |
} | |
} | |
const propsProc = processProp(props, '$props', true) | |
let html = await renderToStringAsync(() => input(propsProc)) | |
for (const [key, value] of Object.entries(postProcReplacements)) { | |
html = html.replaceAll(key, value) | |
} | |
/** | |
* Unbelievably insane logic that converts an object into a Psalm-compatible type definition. | |
* @param toProcess The object to generate a type definition for | |
* @param pretty Whether to pretty-print the output | |
* @returns The type definition for the object | |
*/ | |
function propsToPhpDoc(toProcess: any, pretty: boolean): string { | |
return ( | |
'\n' + | |
JSON.stringify(toProcess, (k, v) => { | |
if (v == null) { | |
return 'null' | |
} | |
if (k === '' && typeof toProcess === 'object' && !Array.isArray(toProcess)) { | |
return v | |
} | |
if (Array.isArray(v)) { | |
const types = new Set(v.map(x => { | |
let str = propsToPhpDoc(x, false) | |
.replaceAll('"', '') | |
if (str.startsWith('\n')) { | |
str = str.substring(1) | |
} | |
return str | |
})) | |
if (types.size === 0) { | |
return 'array' | |
} else { | |
return 'array<' + Array.from(types).join('|') + '>' | |
} | |
} | |
if (typeof v === 'number') { | |
return 'numeric' | |
} | |
if (typeof v === 'boolean') { | |
return 'bool' | |
} | |
if (typeof v === 'object') { | |
return v | |
} | |
return typeof v | |
}, pretty ? 4 : undefined) | |
) | |
.replaceAll('\n{', 'array{') | |
.replaceAll(' {', ' array{') | |
} | |
return `/**\n * @param ${propsToPhpDoc(props, true).split('\n').join('\n * ').replaceAll('"', '')} $props Template props\n */\nfunction ${functionName}(array $props) { ?>${html}<?php }\n` | |
} | |
const outPath = join(__dirname, '../baked/header.php') | |
await writeFile(outPath, ( | |
'<?php\n' + await bakeToString(Header, { | |
title: '', | |
hasNotifications: false, | |
notificationsCount: 0, | |
}, { | |
functionName: 'renderHeader', | |
sanitizerFunc: 'htmlspecialchars', // htmlspecialchars is unsafe without ENT_QUOTES, so this should be your own function instead | |
}) | |
)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, JSX } from 'solid-js' | |
type ShowProps = { | |
when: boolean, | |
children: JSX.Element | |
fallback?: JSX.Element | |
} | |
/** | |
* Polyfill for the Solid.js `Show` component that works with Bake. | |
*/ | |
export const Show: Component<ShowProps> = typeof process === 'object' && process.env.RUN_MODE === 'bake' | |
? (props) => { | |
return <>__PHP__IF__{props.when}__PHP__THEN__{props.children}__PHP__ELSE__{props.fallback}__PHP__ENDIF__</> | |
} | |
: (props) => { | |
if (props.when) { | |
return props.children | |
} else if (props.fallback) { | |
return props.fallback | |
} else { | |
return undefined | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment