competent
Extracts, Renders And Exports For Dynamic Render JSX Components From Within HTML.
yarn add competent
Table Of Contents
- Table Of Contents
- API
competent(components, config=): !_restream.Rule
- Additional Methods
DEBUG=competent
makeComponentsScript(components, options=): string
async writeAssets(path): void
- Known Limitations
- Who Uses Competent
- License & Copyright
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.
- <kbd>components*</kbd>
!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. - <kbd>config</kbd>
!Config
(optional): Options for the program. All functions will be called with the Replaceable instance as theirthis
context.
Example Usage |
---|
html
<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>
|
For example, the above HTML page can be rendered with Competent by creating a Replaceable rule: |
jsx
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
|
The output will contain rendered JSX. |
html
<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>
|
The logging will be output to stderr .
|
js
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.
Name | Type & Description | Default |
---|---|---|
removeOnError | boolean | false |
If there was an error when rendering the component, controls whether the HTML should be be left on the page. | ||
sep | string | os.EOL |
When an array is returned, what line separator to use. | ||
getId | (key: string, props: !Props) => string | |
The function which returns an id for the html element.<kbd>key</kbd> string : Component key.<kbd>props</kbd> !Props : Either HTML props, or properties overriden by .export call.
|
||
getProps | (props: !Props, meta: !Meta, componentName: string, position: number) => Object | |
The function which takes the parsed properties from HTML and competent's meta methods, and returns the properties object to be passed to the component. By default, returns the properties simply merged with meta. <kbd>props</kbd> !Props : Properties.<kbd>meta</kbd> !Meta : Meta properties.<kbd>componentName</kbd> string : The name of the component.<kbd>position</kbd> number : The position where match happened.
|
||
markExported | (key: string, id: string, props: !Props, children: !Array<string>) => ? | |
If the component called the export meta method, this function will be called at the end of the replacement rule with its key, root id, properties and children as strings.<kbd>key</kbd> string : Component key.<kbd>id</kbd> string : The ID assigned manually either via the element's id attribute, or with the getId function automatically.<kbd>props</kbd> !Props : Component properties.<kbd>children</kbd> !Array<string> : Component children.
|
||
onSuccess | (componentName: string, htmlProps: !Object<string, string>) => void | |
The callback at the end of a successful replacement with the component's key. <kbd>componentName</kbd> string : The element name, e.g., my-element .<kbd>htmlProps</kbd> !Object<string, string> : The properties with which the component was initialised.
|
||
onFail | (componentName: string, error: !Error, position: number, input: string) => void | |
The callback at the end of failed replacement with the component's key, error object, position number and the string which was fed to the rule. <kbd>componentName</kbd> string : The element name, e.g., my-element .<kbd>error</kbd> !Error : The error.<kbd>position</kbd> number : The position in the input text where element started.<kbd>input</kbd> string : The input string.
|
||
getContext | (childContext?: !Object, parent: { position: number, key: string }) => !Object | |
The function to be called to get the properties to set on the child Replaceable started to recursively replace inner HTML. This is needed if the root Replaceable was assigned some properties that are referenced in components. <kbd>childContext</kbd> !Object (optional): The child context set by meta.setChildContext with undefined if not set.<kbd>parent</kbd> { position: number, key: string } : The info about the parent component.
|
||
getReplacements | (componentName: string, recursiveRenderAgain: boolean) => !Array<!_restream.Rule> | |
The function which should return the list of replacements for renderAgain method. By default, the initial rule generated by Competent is used. The first argument passed is the key, and the second argument is the value passed via the renderAgain , that is if the component might render recursively.<kbd>componentName</kbd> string : Component key.<kbd>recursiveRenderAgain*</kbd> boolean : The value passed to renderAgain .
|
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
.
Name | Type & Description |
---|---|
export | (shouldExport?: boolean, props?: Object) => void |
When called, marks the component for export and adds an id if the root element of the hyper result did not have it. Individual instances can pass the false value if they don't want to get exported.<kbd>shouldExport</kbd> boolean (optional): Whether to export the component. Default true .<kbd>props</kbd> Object (optional): Properties with which to export. If not passed, the same HTML props are used, otherwise overrides them. Undefined values will be removed.
|
|
skipRender | () => void |
If this method is called, Competent will return the original match without rendering the component into static HTML. This should be used together with export to provide run-time dynamic browser rendering, without static HTML code generation.
|
|
setPretty | (isPretty: boolean, lineLength?: number) => void |
The function which controls whether to enable pretty printing, and the line width. <kbd>isPretty</kbd> boolean : Whether to pretty print.<kbd>lineLength</kbd> number (optional): Number of characters after which to wrap lines.
|
|
removeLine | (shouldRemove?: boolean) => void |
If the component rendered a falsy value (e.g., null , '' ), and the removeLine was called, Competent will remove \n___<component> . By default, this is switched off.<kbd>shouldRemove</kbd> boolean (optional): Sets whether the new line should be removed (default true ).
|
|
renderAgain | (doRender?: boolean, recursiveRender?: boolean) => void |
After rendering the component itself, the children by default are also rendered by spawning another Replaceable stream. This is needed when a component might contain other components when rendered.
recursiveRender is set to false (default), the component key will be excluded from the rule to prevent recursion.<img/> renders <img> (no / ) for example.getReplacements was used to specify how to acquire the replacements for the new child Replaceable stream, the recursiveRender arg will be pased to it.boolean (optional): Whether to render component again to update its inner HTML. Default true .<kbd>recursiveRender</kbd> boolean (optional): Whether to render element with the same name. Default false .
|
|
setChildContext | (context: !Object) => void |
JSX nodes are rendered breadth-first, meaning that siblings will receive the same this context. If one of them modifies it, the another one will also pass the updated one to children, which is not always desirable. To create a fork context unique for children of sibling nodes, the child context can be set. It will be passed as an argument to getContext .<kbd>context</kbd> !Object : The context specific for children of the node that calls renderAgain .
|
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.
Name | Type & Description |
---|---|
constructor | new () => CompetentComponent |
Constructor method. | |
<kbd>static</kbd> load | (callback: function(Error, !Object=): void, element: Element, props: !Object) => void |
<kbd>callback</kbd> function(Error, !Object=): void : A method called by browser-side bundle prior to rendering of a component with a callback, e.g., to load necessary assets. The callback should be called by the component when the loading is done, after which the component will render. The second argument to the callback can be a map of properties that should also be passed to the component.<kbd>element</kbd> Element : The element into which the component will be rendered.<kbd>props</kbd> !Object : The properties that the component will receive.
|
|
plain | boolean |
Whether this is a non-Preact component. This is required since Closure Compiler will compile classes into functions and the .isPrototypeOf won't wort to detect components that shouldn't be rendered with Preact.
|
|
serverRender | (props?: !preact.PreactProps) => (preact.AcceptedChild | !Array<preact.AcceptedChild>) |
The same as render, but for the server only. Called by Component using NodeJS runtime and not by Preact in browser, therefore NodeJS API could be used here. <kbd>props</kbd> !preact.PreactProps (optional): Component properties.
|
|
fileRender | (data: string, props?: !preact.PreactProps) => !Promise<void> |
When serverRender was specified, this method will also render the component using the standard render method, and return the output. The output could then be written by the implementation to the filesystem, e.g., saved as component.html file which is then loaded in browser by load method.<kbd>data</kbd> string : The rendered component.<kbd>props</kbd> !preact.PreactProps (optional): Component properties.
|
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.
- <kbd>components*</kbd>
!Array<!ExportedComponent>
: All components that were made exportable by the rule. - <kbd>options</kbd>
MakeCompsConfig
(optional): The options for the make components script.
MakeCompsConfig
: The options for make components script.
Name | Type & Description | Default |
---|---|---|
map | !Object<string, !Array<?string>> | - |
The map with locations from where components should be imported, e.g.,
js
{
'../components/named.jsx': [null, 'named-component'],
'../components/default.jsx': ['default-component'],
}
The default export must come first in the array.
|
||
io | (boolean | !IOOptions) | false |
Whether to use an IntersectionObserver to render elements. If an object is given, it will be passed to the IO constructor, otherwise the default options are used (rootMargin: '76px' ).
|
||
preact | (string | boolean) | preact |
Whether any of the components are Preact components.
Only pass false when you know for sure that all components implement plain getter.
A string can be passed to name the package from which to import the h pragma (e.g., @externs/preact ).
|
||
props | !Object<string, *> | - |
Shared properties made available for each component in addition to its own properties. | ||
includeH | boolean | false |
Include import { h } from 'preact' on top of the file.
|
||
externalAssets | (boolean | string) | false |
Whether the library functions should be required from a separate file, ./competent-lib . Works together with writeAssets and is useful when generating more than one script. The relative path can be passed as a string, e.g., .. will make ../ competent-lib .
|
IOOptions
extends IntersectionObserverInit: Options for the observer.
Name | Type & Description |
---|---|
constructor | new () => IOOptions |
Constructor method. | |
log | boolean |
Whether to print a message to console when a component is rendered. |
ExportedComponent
: An exported component.
Name | Type | Description |
---|---|---|
key* | string | The name of the component as passed to Competent. |
id* | string | The ID where the component should render. |
props* | !Object | Properties 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
- <kbd>path*</kbd>
string
: The folder where to create the__competent-lib.js
file, when theexternalAssets
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
license@artd.eco
for more information.
![]() |
© Art Deco™ 2020 |
---|