WebReflection / uhtml

A micro HTML/SVG render
MIT License
903 stars 37 forks source link

uhtml/ssr - Added both easy SSR and worker.js target #114

Closed WebReflection closed 5 months ago

WebReflection commented 5 months ago

This MR brings forward the hydro branch https://github.com/WebReflection/uhtml/pull/113 as I got stuck but also I am not sure anymore I was doing the right thing ... but as I don't want to get stuck, it made sense to at least provide an uhtml/ssr export and fix some gotcha with the persistent fragment.

This MR also fixes https://github.com/WebReflection/uhtml/issues/111 by removing TypeScirpt as a whole and provide just JSDoc TS for the provided files. I hope this won't break anyone out there but as types were buggy this can land as minor as it doesn't break anything, it adds a new uhtml/ssr export, and it should improve TS / types in general.

The hydro branch is on hold until I have both head and time to get that right.

iacore commented 5 months ago

Thanks! It works well! Maybe you will be interested in the final product.

iacore commented 5 months ago

I used uhtml/ssr a bit more, and... my code ended up like this:

/**
 * @function
 * @param {import('uhtml/keyed')} */
module.exports = ({ Hole, attr, html, htmlFor, render, svg, svgFor, document }) => ({
  root() {
    const body = html`
      <p>Hi!</p>
    `
    return html`<head><link rel="stylesheet" href="/all.css" /></head><body>${body}</body>`
  }
})

given how document is only available at run-time, this API is not .. great. Also, the return type of initSsr is inferred as any in vscode; not sure why.

Is it possible for a html` template to exist withoutdocument`?

WebReflection commented 5 months ago

@iacore I am not sure I am following ... see the test example: https://github.com/WebReflection/uhtml/blob/main/test/ssr.mjs

you can have one or more documents (one per page to render if you want) and I can't provide a global document as that's hostile.

I have removed types and used the suggested way to export things in there ... I don't know why TS doesn't understand that but I wonder if you are on latest TS.

As follow up questions:

module.exports = typeof process === 'object' ?
  require('uhtml/ssr')('...') :
  { ...require('uhtml/keyed'), document };

This uhtml.cjs would give you same exports if you are either on Node/Bun or Browser.

Make it an .mjs or a .js with type: "module" if you're after ESM.

Would this improve the API?

iacore commented 5 months ago

As follow up questions:

* what/how do you want to use the `document` if shared? it leads to weird concurrent cases if all pages share the same document

I don't want to use document if shared, although node.js is single-threaded so this would not be a problem if there is only one render call.

* is the issue that you can't have globally available `html` out of a module?

This is the issue. uhtml does not allow components to be defined with a generic html function; it has to be bound to a document.

In preact, the components are defined with JSX/hyperscript, so this is not a problem.

So my question is, can Hole live without document? Is this limitation of uhtml's design?


For the types, I will fix it.

WebReflection commented 5 months ago

you need to import html and render regardless of the SSR story, that's how modules work in JS and uhtml is just a module so I keep not being sure I follow the question.

The Hole is just a class and represents entries expected as holes within the render logic. Neither the render nor the uhtml or other exports are global but where do you have an issue with it? JSX requires transpilation (even in Bun) and it works with pragma functions that default to React namespace which is also global ... none of this is part of uhtml as the whole point is to use standards (ESM, template literal tags, no tools needed) but your files need to import those utilities and I've suggested a way to do so, out of other ways, if sharable components is what you are after.

iacore commented 5 months ago

Here is a more concrete example.

Here's a file, abc.js.

import { html, render } from 'uhtml'

html`<h1>a</h1>`

If I run it with node, it doesn't work.

❯ node abc.js
file:///home/user/computing/lib/js/uhtml/esm/persistent-fragment.js:30
export class PersistentFragment extends custom(DocumentFragment) {
                                               ^

ReferenceError: DocumentFragment is not defined
    at file:///home/user/computing/lib/js/uhtml/esm/persistent-fragment.js:30:48
    at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:323:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:120:12)

Node.js v21.7.2

Can this limitation be overcome?

IMO, the definition of a component (even as simple as html`\

a\

`) should not depend on the DOM environment to exist. i.e. DOM would be needed only when the component is rendered, not when it is defined.

I think I will read the source code of uhtml to understand how it works.

WebReflection commented 5 months ago

Can this limitation be overcome?

yes, that's why you want either uhtml/init or uhtml/ssr ... if you want that to work in both client or server you need to create your own file as explained in here https://github.com/WebReflection/uhtml/pull/114#issuecomment-2056150399 as I can't provide uhtml without a Document on NodeJS side and you don't want a shared document for multiple pages purpose unless you use a template for the <head> part and you only render different bodies, that works too, still you gotta provide a document.

If you are OK in polluting the global context then in node only:

import initSSR from 'uhtml/ssr';

const { document } = initSSR('...');
Object.assign(globalThis, {
  document,
  DocumentFragment: document.createDocumentFragment().constructor
});

Then you should be able to use that file and eventually serve document.toString() after render(document.body, () => htmlyour thing)

edit P.S. that entry point should run before the rest of your node app runs or use .cjs instead so that the resolution of each module is incremental.

WebReflection commented 5 months ago

@iacore another alternative is to use importMap so that on browser you get just that while on node you get your file that bootstrap the SSR and exports then everything it offers, after placing the document and the fragment to the global, then you should have a seamless exprience. I can't remember the current state of importMap in node though.

What I do in my tests is like this: https://github.com/WebReflection/uhtml/blob/main/test/modern.mjs#L9-L13

iacore commented 5 months ago

What I do in my tests is like this: https://github.com/WebReflection/uhtml/blob/main/test/modern.mjs#L9-L13

Thanks! This is exactly what I'm looking for!