Skip to content

Front End

Anton edited this page Mar 12, 2020 · 12 revisions

Latest browsers support pretty much all JavaScript features that are used to write modern code. Therefore, if a front-end package publishes its source code, it's possible to just execute it in the browser, without compiling or transpiling it. The only problem is that browser won't be able to handle import packageName from 'package-name', since modules must be imported by path. Therefore, the frontend middleware will patch the source code to update those cases.

This middleware also allows to serve JSX files by transpiling them on-the-fly using a sinple JSX parser (no proper ASTs). Although there are some limitations, the parser works great

Front End options just extend the Front End configuration record, providing the .use method.

FrontEndOptions extends FrontEndConfig: Options for the frontend.

Name Type & Description Default
use boolean false
Use this middleware for every request.
hotReload (boolean | HotReloadOptions) -
Options to enable hot reload of exported functions and classes. Can simply pass true to activate.

FrontEndConfig: Options for the middleware.

Name Type & Description Default
directory (string | !Array<string>) frontend
The directory or directories from which to serve files.
mount string .
The directory on which to mount. The dirname must be inside the mount. E.g., to serve example/src/index.js from /src/index.js, the mount is example/src and directory is src.
override !Object<string, string> -
Instead of resolving the package.json path for packages and looking up the module and main fields, paths can be passed manually in the override. E.g., { preact: '/node_modules/preact/src/preact.js' } will serve the source code of Preact instead of the resolved dist version.
pragma string import { h } from 'preact'
The pragma function to import. This enables to skip writing h at the beginning of each file. JSX will be transpiled to have h pragma, therefore to use React it's possible to do import { createElement: h } from 'react'.
log (boolean | !Function) false
Log to console when source files were patched.
jsxOptions !_alaJsx.Config -
Options for the transpiler.
exportClasses boolean true
When serving CSS, also export class names.
hotReload !HotReload -
Enable hot reload for modules. Requires at least to implement getServer method so that WebSocket listener can be set up on the HTTP server.

The middleware will assign the etag on the response which is equal to the date when the patched file changed. This means that the browser will cache the file without the middleware having to do the work again which is useful to speed up development by preserving memory.

Example

By default, the frontend directory is used to serve files, but it can be changes and multiple folders can be specified in the config. Files can be served without the extension, and when requesting a directory, the middleware will redirect to the index file. Scripts can also import CSS styles, and the middleware will serve them via dynamic JS.

const { url, app } = await idio({
  async log(ctx, next) {
    console.log('//', ctx.method, ctx.path)
    await next()
  },
  frontend: {
    use: true,
    directory: [
      'wiki/Front-End/frontend',
      'example/frontend',
    ],
  },
})
// GET /wiki/Front-End/frontend/Example
import { h } from '/node_modules/preact/dist/preact.mjs'
import { Component } from '/node_modules/preact/dist/preact.mjs'
import { $Example } from './style.css'

export default class Example extends Component {
  render() {
    return (h('div',{className:$Example},
      `Idio Web Server.`
    ))
  }
}

// GET /wiki/Front-End/frontend/index
import { h } from '/node_modules/preact/dist/preact.mjs'
import { render } from '/node_modules/preact/dist/preact.mjs'
import Example from './Example'

render(Example, document.body)
// GET /wiki/Front-End/frontend
/*
 * status: 302,
 * location: /wiki/Front-End/frontend/index.jsx 
 */

// GET /wiki/Front-End/frontend/style.css
(function Re(a = "") {
  let b;
  window["wiki-Front-End-frontend-style"] ? (b = window["wiki-Front-End-frontend-style"], b.innerText = "") : (b = document.createElement("style"), b.id = "wiki-Front-End-frontend-style", document.head.appendChild(b));
  b.type = "text/css";
  b.styleSheet ? b.styleSheet.cssText = a : b.appendChild(document.createTextNode(a));
})(`body {
  font-size: large;
}
.Example {
  color: ghostwhite;
}`)
export const $Example = 'Example'

Class names can be extracted from stylesheets also using import statements. Front End has very basic class detection name. This feature can be used together with ÀLaMode when building package for publishing, and Depack when creating deployable web bundles. The point of importing classes like that is that they can be renamed with Closure Stylesheets.

Mounting

The paths used to serve front-end files will have to contain the path to the frontend dir passed during configuration. To mount them on a separate path, the mount config option can be used, however the served folder must be inside of the mount point as shown below.

const { url, app } = await idio({
  async log(ctx, next) {
    console.log('//', ctx.method, ctx.path)
    await next()
  },
  // serves from example/wiki/frontend at /frontend
  frontend: {
    use: true,
    directory: 'frontend',
    mount: 'wiki/Front-End',
  },
}, { port: null })
// GET /frontend/index
import { h } from '/../../node_modules/preact/dist/preact.mjs'
import { render } from '/../../node_modules/preact/dist/preact.mjs'
import Example from './Example'

render(Example, document.body)

Hot Reload

The middleware supports native module reload, via a simple hack: we'll add a very short code at the bottom of each served file that will update exported bindings from that file. If a function is updated, it will be replaced, whereas if a class is updated, it's prototype will receive new properties.

It's recommended to install the node-watch dependency for this feature, as native Node watching is pretty buggy and some IDEs fire update events twice..

const { url, app } = await idio({
  async log(ctx, next) {
    console.log('//', ctx.method, ctx.path)
    await next()
  },
  frontend: {
    use: true,
    directory: 'wiki/Front-End/frontend',
    hotReload: true, // enable reload
  },
})
// GET /wiki/Front-End/frontend/Example
import { h } from '/node_modules/preact/dist/preact.mjs'
import { Component } from '/node_modules/preact/dist/preact.mjs'
import { $Example } from './style.css'

export default class Example extends Component {
  render() {
    return (h('div',{className:$Example},
      `Idio Web Server.`
    ))
  }
}

/* IDIO HOT RELOAD */
import { idioHotReload } from '/hot-reload'
if (idioHotReload) {
  let _idio = 0
  idioHotReload('wiki/Front-End/frontend/Example.jsx', async () => {
    _idio++
    const module = await import(`./Example?ihr=${_idio}`)

    return {
      module,
      classes: {
        'default': Example,
      },
    }
  })
}

🖥 Read more

TODO & JSX Limitations

  1. We could also transpile require statements into imports so that the browser can serve them properly, without having to polyfill the require method.
  2. Comments are supported as long as {} and <> are balanced within them which is pretty normal if you're commenting blocks out.
    import { render }  from 'preact'
    
    const example = 'hello'
    render(<div>
      {/* hello world */}
      {/* <span>Comment {example}</span> */}
    </div>)
    import { render }  from 'preact'
    
    const example = 'hello'
    render(   h('div',{},
      /* hello world */
      /* <span>Comment {example}</span> */
    ))
  3. No curly braces in components' attributes are allowed.
    render(<div title="hello{world}"/>)
    /Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:194
          throw Error("Could not detect prop name");
          ^
    
    Error: Could not detect prop name
        at /Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:194:13
        at Array.reduce (<anonymous>)
        at P (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:189:13)
        at W (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:409:27)
        at /Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:444:7
        at Object.<anonymous> (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:446:3)
        at Module._compile (internal/modules/cjs/loader.js:1158:30)
        at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
        at Object.h.<computed>.y._extensions.<computed> [as .js] (/Users/zavr/idiocc/idio/node_modules/alamode/compile/depack.js:51:7)
        at Module.load (internal/modules/cjs/loader.js:1002:32)
  4. No > sign inside components is permitted. Use &gt or take comparisons outside JSX tag.
    // won't work
    render(<div title="hello > world">
      {(length > 10) && <span>Next Page</span>}
    </div>)
    evalmachine.<anonymous>:3
      ,(length > 10) && h('span',{},`Next Page`),
                                     ^^^^
    
    SyntaxError: Unexpected identifier
        at new Script (vm.js:88:7)
        at E (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:84:5)
        at W (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:402:11)
        at W (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:416:10)
        at /Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:444:7
        at Object.<anonymous> (/Users/zavr/idiocc/idio/node_modules/@a-la/jsx/depack/jsx.js:446:3)
        at Module._compile (internal/modules/cjs/loader.js:1158:30)
        at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
        at Object.h.<computed>.y._extensions.<computed> [as .js] (/Users/zavr/idiocc/idio/node_modules/alamode/compile/depack.js:51:7)
        at Module.load (internal/modules/cjs/loader.js:1002:32)
    // updated
    const hasNextPage = length > 10
    render(<div title="hello &gt; world">
      {hasNextPage && <span>Next Page</span>}
    </div>)
You can’t perform that action at this time.