3.8.0 • Published 4 years ago

competent v3.8.0

Weekly downloads
79
License
AGPL-3.0
Repository
gitlab
Last release
4 years ago

Competent

npm version

competent Extracts, Renders And Exports For Dynamic Render JSX Components From Within HTML.

yarn add competent

Table Of Contents

API

The package is available by importing its default and named functions:

import competent, { makeComponentsScript, writeAssets } from 'competent'

competent(  components: !Object<string, !Function|function(new: preact.Component)>,  config=: !Config,): !_restream.Rule

Creates a rule for Replaceable from the restream package that replaces HTML with rendered JSX components. The configuration object will be needed to export components, so that they can then be rendered on the page using JavaScript.

  • components* !Object<string, (!Function | function(new: preact.Component))>: Components to extract from HTML and render using Preact's server-side rendering. Can be either a functional stateless component, or a Preact component constructor.
  • config !Config (optional): Options for the program. All functions will be called with the Replaceable instance as their this context.
<html lang="en">

<npm-package style="background:red;">splendid</npm-package>
<npm-package style="background:green;">@a-la/jsx</npm-package>
<npm-package style="background:grey;">unknown-package</npm-package>

<hello-world from="Art Deco">
  An example usage of competent.
</hello-world>
<friends count="10"/>
</html>
import competent from 'competent'
import aqt from '@rqt/aqt'
import read from '@wrote/read'
import { Replaceable } from 'restream'

/**
 * A standard JSX component.
 */
const HelloWorld = ({ from, children, competent: c }) => {
  c.setPretty(false)
  return (<p>Hello World From {from}.{children}</p>)
}

/**
 * A string component.
 */
const FriendCount = ({ count }) => {
  return `You have ${count} friends.`
}

/**
 * An async component.
 */
const NpmPackage = async ({ style, children, competent: c }) => {
  c.export()
  let [pck] = children
  pck = encodeURIComponent(pck)
  const { statusCode, body } =
    await aqt('https://registry.npmjs.com/' + pck)
  if (statusCode == 404) throw new Error(`Package ${pck} not found.`)
  const { name, versions, description } = body
  const keys = Object.keys(versions)
  const version = keys[keys.length - 1]
  return <div style={style}>
    <span className="name">{name}</span>
    <span className="ver">{version}</span>
    <p>{description}</p>
  </div>
}

const CompetentExample = async () => {
  let i = 0
  const exported = []
  const file = await read('example/index.html')
  const rule = competent({
    'hello-world': HelloWorld,
    'npm-package': NpmPackage,
    'friends': FriendCount,
  }, {
    getId() {
      i++
      return `c${i}`
    },
    getProps(props, meta) {
      meta.setPretty(true, 60)
      return { ...props, competent: meta }
    },
    onFail(key, err) {
      console.error('Component %s did not render:', key)
      console.error(err.message)
    },
    markExported(key, id, props, children) {
      exported.push({ key, id, props, children })
    },
  })
  const r = new Replaceable(rule)
  const res = await Replaceable.replace(r, file)
  return { res, exported }
}

export default CompetentExample
<html lang="en">

<div style="background:red;" id="c1">
  <span class="name">splendid</span>
  <span class="ver">1.19.0</span>
  <p>
    Static Web Site Compiler That Uses Closure Compiler For JS Bundling And Closure Stylesheets For CSS optimisations. Supports JSX Syntax To Write Static Elements And Dynamic Preact Components.
  </p>
</div>
<div style="background:green;" id="c2">
  <span class="name">@a-la/jsx</span>
  <span class="ver">1.8.0</span>
  <p>The JSX Transform For ÀLaMode And Other Packages.</p>
</div>
<npm-package style="background:grey;">unknown-package</npm-package>

<p>Hello World From Art Deco.
  An example usage of competent.
</p>
You have 10 friends.
</html>
Component npm-package did not render:
Package unknown-package not found.
Exported packages:
[
  {
    key: 'npm-package',
    id: 'c1',
    props: { style: 'background:red;' },
    children: [ 'splendid' ]
  },
  {
    key: 'npm-package',
    id: 'c2',
    props: { style: 'background:green;' },
    children: [ '@a-la/jsx' ]
  }
]

Config: Options for the program. All functions will be called with the Replaceable instance as their this context.

The meta methods are usually used by the components in the render/serverRender methods, to control how the specific component instance should be rendered. If the getProps is not passed in the config, by default they will extend the HTML properties of the component.

Meta: Service methods for competent.

Additional Methods

Competent can work with additional API of components, in which case they must extend the Preact class and implement these additional methods.

CompetentComponent extends preact.Component: A component could have an additional API understood by Competent.

For example, we could implement a component that loads additional libraries and JSON data, and only renders when they are ready in the following way:

/* eslint-env browser */
import loadScripts from '@lemuria/load-scripts'
import { Component } from 'preact'

export default class Menu extends Component {
  /**
   * @suppress {checkTypes}
   */
  static 'load'(callback) {
    loadScripts([
      'js/menu.json',
      'snapsvg/dist/snap.svg-min.js',
      'js/svg-anim.js',
    ], (err, res) => {
      if (err) return callback(err)
      try {
        const [json] = /** @type {!Array<string>}*/ (res)
        callback(null, { json: JSON.parse(json) })
      } catch (er) {
        callback(er)
      }
    })
  }
  serverRender({ splendid }) {
    splendid.export()
    splendid.addFile('js/menu.json')
    splendid.addFile('js/svg-anim.js.map')
    splendid.addFile('img/menu.svg')
    splendid.polyfill('replace-with', true)
    splendid.addExtern('node_modules://@artdeco/snapsvg-animator/types/externs.js')
    return (<div id="menu" style="width:100%;">
      <img style="max-width:100%;" alt="menu" src="img/menu.svg" />
    </div>)
  }
  render({ json }) {
    const width = 1226
    const height = 818

    /** @type {!_snapsvgAnimator.SVGAnim} */
    const comp = new window['SVGAnim'](json, width, height)
    const n = comp.s.node
    n.style['max-width'] = '100%'

    return (<div id="menu" style="width:100%;" ref={(el) => {
      el.appendChild(n)
    }}/>)
  }
}

When compiling with Closure Compiler (or Depack), the static methods need to be written in quotes like static 'method'(), otherwise Closure will rename them. The checkTypes warning should also be suppressed. The other way to do that would be to write static methods normally, but then reassign them: Example['staticMethod'] = Example.staticMethod;

DEBUG=competent

When the DEBUG env variable is set to competent, the program will print some debug information, e.g.,

2020-04-08T05:45:14.420Z competent render npm-package
2020-04-08T05:45:14.441Z competent render npm-package
2020-04-08T05:45:14.442Z competent render npm-package
2020-04-08T05:45:14.445Z competent render hello-world
2020-04-08T05:45:14.447Z competent render friends

makeComponentsScript(  components: !Array<!ExportedComponent>,  options=: MakeCompsConfig,): string

Based on the exported components that were detected using the rule, generates a script for the web browser to dynamically render them with Preact.

  • components* !Array<!ExportedComponent>: All components that were made exportable by the rule.
  • options MakeCompsConfig (optional): The options for the make components script.

MakeCompsConfig: The options for make components script.

The map with locations from where components should be imported, e.g.,

{
  '../components/named.jsx': [null, 'named-component'],
  '../components/default.jsx': ['default-component'],
}

The default export must come first in the array.

IOOptions extends IntersectionObserverInit: Options for the observer.

ExportedComponent: An exported component.

NameTypeDescription
key*stringThe name of the component as passed to Competent.
id*stringThe ID where the component should render.
props*!ObjectProperties of the component.
children*!Array<string>Children as strings.
import CompetentExample from './'
import { makeComponentsScript } from 'competent'

(async () => {
  const { exported } = await CompetentExample()
  console.log(makeComponentsScript(exported, {
    map: {
      '../components/npm': ['npm-package'],
      // default first then named
      '../components': ['hello-world', 'friends'],
    },
  }))
})()
import { Component, render } from 'preact'
import NpmPackage from '../components/npm'

const __components = {
  'npm-package': NpmPackage,
}

function init(id, key) {
  const el = document.getElementById(id)
  if (!el) {
    console.warn('Parent element for component %s with id %s not found', key, id)
    return {}
  }
  const parent = el.parentElement
  if (!parent) {
    console.warn('Parent of element for component %s with id %s not found', key, id)
    return {}
  }
  return { parent, el  }
}

class PreactProxy {
  /**
   * Create a new proxy.
   * @param {Element} el
   * @param {Element} parent
   * @param {*} Comp
   * @param {*} preact
   */
  constructor(el, parent, Comp, preact) {
    this.preact = preact
    this.Comp = Comp
    this.el = el
    this.parent = parent
    /**
     * A Preact instance.
     */
    this.comp = null
    this.unrender = null
  }
  render({ children, ...props }) {
    if (!this.comp) {
      this.preact.render(this.preact.h(this.Comp, props, children), this.parent, this.el)
      const comp = this.el['_component']
      if (comp.componentWillUnmount) {
        this.unrender = () => {
          comp.componentWillUnmount()
        }
      }
      this.comp = comp
    } else {
      if (this.comp.componentDidMount) this.comp.componentDidMount()
      this.comp.forceUpdate()
    }
  }
}

function start(meta, Comp, comp, el, parent, props, children, preact) {
  const isPlain = meta.plain
  if (!comp && isPlain) {
    comp = new Comp(el, parent)
  } else if (!comp) {
    comp = new PreactProxy(el, parent, Comp, preact)
  }
  const r = () => {
    comp.render({ ...props, children })
    meta.instance = comp
  }
  if (Comp.load) {
    Comp.load((err, data) => {
      if (data) Object.assign(props, data)
      if (!err) r()
      else console.warn(err)
    }, el, props)
  } else r()
  return comp
}

/** @type {!Array<!preact.PreactProps>} */
const meta = [{
  key: 'npm-package',
  id: 'c2',
  props: {
    style: 'background:green;',
  },
  children: ["@a-la/jsx"],
},
{
  key: 'npm-package',
  id: 'c1',
  props: {
    style: 'background:red;',
  },
  children: ["splendid"],
}]
meta.forEach(({ key, id, props = {}, children = [] }) => {
  const Comp = __components[key]
  const plain = Comp.plain || (/^\s*class\s+/.test(Comp.toString()) && !Component.isPrototypeOf(Comp))

  const ids = id.split(',')
  ids.forEach((Id) => {
    const { parent, el } = init(Id, key)
    if (!el) return
    const renderMeta = /** @type {_competent.RenderMeta} */ ({ key, id: Id, plain })
    let comp
    comp = start(renderMeta, Comp, comp, el, parent, props, children, { render, Component, h })
  })
})

There are Plain and Preact components. By default, the assumption is that there are Preact components in the map passed in options. When preact option is set to false, only plain logic is enabled, skipping the Preact imports and externs.

Assets

By default, the lib functions will be embedded into the source code. To place them in separate files for reuse across multiple generated scripts, the externalAssets option is used together with writeAssets method.

Intersection Observer

Competent can generate code that will utilise the IntesectionObserver browser capability to detect when the element into which the components needs to be rendered comes into view, and only mount it at that point. This will only work when IntesectionObserver is present either natively, or via a polyfill. When the io argument value is passed as an object rather than boolean, it will be serialised, e.g., { rootMargin: '0 0 76px 0' }.

import CompetentExample from './'
import { makeComponentsScript } from 'competent'

(async () => {
  const { exported } = await CompetentExample()
  console.log(
    makeComponentsScript(exported, {
      map: {
        '../components/npm': ['npm-package'],
        '../components': ['hello-world', 'friends'],
      },
      io: { threshold: 10, rootMargin: '50px' },
      externalAssets: true,
    })
  )
})()
import { Component, render } from 'preact'
import { makeIo, init, start } from './__competent-lib'
import NpmPackage from '../components/npm'

const __components = {
  'npm-package': NpmPackage,
}

const io = makeIo({ threshold: 10, rootMargin: "50px" })

/** @type {!Array<!preact.PreactProps>} */
const meta = [{
  key: 'npm-package',
  id: 'c2',
  props: {
    style: 'background:green;',
  },
  children: ["@a-la/jsx"],
},
{
  key: 'npm-package',
  id: 'c1',
  props: {
    style: 'background:red;',
  },
  children: ["splendid"],
}]
meta.forEach(({ key, id, props = {}, children = [] }) => {
  const Comp = __components[key]
  const plain = Comp.plain || (/^\s*class\s+/.test(Comp.toString()) && !Component.isPrototypeOf(Comp))

  const ids = id.split(',')
  ids.forEach((Id) => {
    const { parent, el } = init(Id, key)
    if (!el) return
    const renderMeta = /** @type {_competent.RenderMeta} */ ({ key, id: Id, plain })
    let comp
    el.render = () => {
      comp = start(renderMeta, Comp, comp, el, parent, props, children, { render, Component, h })
      return comp
    }
    el.render.meta = renderMeta
    io.observe(el)
  })
})

Unrender

When a plain component implements an unrender method, Competent will call it when the component is no longer intersecting. Components that don't provide the unrender method won't be destroyed.

When it comes to Preact component, the same applies, but the unrender method is called componentWillUnmount. Here, an instance will get a chance to remove event listeners and tidy up so that the page keeps performant. The component won't actually be unmounted, because that requires removing the element into which it is rendered from DOM, which can be inefficient and would result in page jumps. Instead, the componentWillUnmount will be called and the component should change its state so that it becomes invisible or a similar measure. Whenever the component comes back into view, its componentDidMount will be called again, and an update scheduled.

/**
 * Example implementation of Preact unrender.
 */
export default class Test extends Component {
  constructor() {
    super()
    this.state.ellipsis = false
  }
  componentDidMount() {
    this.setState({ ellipsis: true })
  }
  componentWillUnmount() {
    this.setState({ ellipsis: false })
  }
  render() {
    return (<span>Hello World{this.state.ellipsis && <Ellipsis />}</span>)
  }
}

async writeAssets(  path: string,): void

  • path* string: The folder where to create the __competent-lib.js file, when the externalAssets option is passed to makeComps.
import { writeAssets } from 'competent'

(async () => {
  await writeAssets('example')
})()
export function init(id, key) {
  const el = document.getElementById(id)
  if (!el) {
    console.warn('Parent element for component %s with id %s not found', key, id)
    return {}
  }
  const parent = el.parentElement
  if (!parent) {
    console.warn('Parent of element for component %s with id %s not found', key, id)
    return {}
  }
  return { parent, el  }
}

export function makeIo(options = {}) {
  const { rootMargin = '76px', log = true, ...rest } = options
  const io = new IntersectionObserver((entries) => {
    entries.forEach(({ target, isIntersecting }) => {
      /**
       * @type {_competent.RenderMeta}
       */
      const meta = target.render.meta
      const { key, id, plain } = meta
      if (isIntersecting) {
        if (log)
          console.warn('🏗 Rendering%s component %s into the element %s',
            !plain ? ' Preact' : '', key, id, target)
        try {
          const instance = target.render()
          if (instance && !instance.unrender) io.unobserve(target) // plain
        } catch (err) {
          if (log) console.warn(err)
        }
      } else if (meta.instance) {
        if (log)
          console.warn('💨 Unrendering%s component %s from the element %s',
            !plain ? ' Preact' : '', key, id, target)
        meta.instance.unrender()
      }
    })
  }, { rootMargin, ...rest })
  return io
}

/**
 * @param {_competent.RenderMeta} meta
 * @param {function(new:_competent.PlainComponent, Element, Element)} Comp
 */
export function startPlain(meta, Comp, comp, el, parent, props, children) {
  if (!comp) comp = new Comp(el, parent)
  const r = () => {
    comp.render({ ...props, children })
    meta.instance = comp
  }
  if (Comp.load) { // &!comp
    Comp.load((err, data) => {
      if (data) Object.assign(props, data)
      if (!err) r()
      else console.warn(err)
    }, el, props)
  } else r()
  return comp
}

/**
 * This is the class to provide render and unrender methods via standard API
 * common for Preact and Plain components.
 */
class PreactProxy {
  /**
   * Create a new proxy.
   * @param {Element} el
   * @param {Element} parent
   * @param {*} Comp
   * @param {*} preact
   */
  constructor(el, parent, Comp, preact) {
    this.preact = preact
    this.Comp = Comp
    this.el = el
    this.parent = parent
    /**
     * A Preact instance.
     */
    this.comp = null
    this.unrender = null
  }
  render({ children, ...props }) {
    if (!this.comp) {
      this.preact.render(this.preact.h(this.Comp, props, children), this.parent, this.el)
      const comp = this.el['_component']
      if (comp.componentWillUnmount) {
        this.unrender = () => {
          comp.componentWillUnmount()
        }
      }
      this.comp = comp
    } else {
      if (this.comp.componentDidMount) this.comp.componentDidMount()
      this.comp.forceUpdate()
    }
  }
}

/**
 * @param {_competent.RenderMeta} meta
 */
export function start(meta, Comp, comp, el, parent, props, children, preact) {
  const isPlain = meta.plain
  if (!comp && isPlain) {
    comp = new Comp(el, parent)
  } else if (!comp) {
    comp = new PreactProxy(el, parent, Comp, preact)
  }
  const r = () => {
    comp.render({ ...props, children })
    meta.instance = comp
  }
  if (Comp.load) {
    Comp.load((err, data) => {
      if (data) Object.assign(props, data)
      if (!err) r()
      else console.warn(err)
    }, el, props)
  } else r()
  return comp
}

Known Limitations

Currently, it is not possible to match nested components.

<Component>
  <Component example />
  <Component test boolean></Component>
</Component>
<component-processed />
</component>

This is because the RegExp is not capable of doing that sort of thing, because it cannot balance matches, however when Competent switches to a non-regexp parser it will become possible.

Who Uses Competent

Competent is used by:

  • Documentary: a documentation pre-processor that supports JSX for reusable components when generating README files.
  • Splendid: a static website generator that allows to write JSX components in HTML, and bundles JS compiler with Google Closure Compiler to also dynamically render them on the page.

License & Copyright

GNU Affero General Public License v3.0

  • You can use Competent as dev dependency to render HTML code, and the components invocation script in any personal/commercial project. For example, you can build a website for your client by writing a script that uses Competent to generate HTML.
  • However, you cannot use the software as part of an online service such as a cloud website builder because then it's not a dev dependency that you run to generate HTML of your website once, but a runtime-linking prod dependency.
  • In other words, when you need to link to the package during runtime, i.e., use it as a standard dependency in your own software (even if bundled), you're creating an extension of the main program with this plugin, and thus have to release your source code under AGPL, or obtain the commercial license.
  • Contact license@artd.eco for more information.
3.8.0

4 years ago

3.7.3

4 years ago

3.7.2

4 years ago

3.7.1

5 years ago

3.7.0

5 years ago

3.6.2

5 years ago

3.6.1

5 years ago

3.6.0

5 years ago

3.5.0

5 years ago

3.4.0

5 years ago

3.3.1

5 years ago

3.3.0

5 years ago

3.2.6

5 years ago

3.2.5

5 years ago

3.2.4

5 years ago

3.2.3

5 years ago

3.2.2

5 years ago

3.2.1

5 years ago

3.2.0

5 years ago

3.1.0

5 years ago

3.0.1

5 years ago

3.0.0

5 years ago

2.2.0

5 years ago

2.1.1

5 years ago

2.1.0

5 years ago

2.0.0

5 years ago

1.8.1

5 years ago

1.8.0

5 years ago

1.7.0

5 years ago

1.6.0

5 years ago

1.5.1

5 years ago

1.5.0

5 years ago

1.4.0

5 years ago

1.3.1

5 years ago

1.3.0

5 years ago

1.2.0

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago