Skip to content

Instantly share code, notes, and snippets.

@termermc
Created November 22, 2024 06:47
Show Gist options
  • Save termermc/e544708b4ecdb4fac9a23d88da1ef6b4 to your computer and use it in GitHub Desktop.
Save termermc/e544708b4ecdb4fac9a23d88da1ef6b4 to your computer and use it in GitHub Desktop.
Solid.js TSX component to PHP template function transpiler (proof of concept)
// 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
})
))
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