Prism Compiler
Prism is a experimental compiler that takes declarative component definitions and creates lightweight web apps. Prism is not a stable production framework, instead a proof of concept of a better isomorphic implementations. Prism is built from the ground up. All HTML, CSS and JS parsing and rendering is done under a internal library known as chef.
Install with:
> npm install -g @kaleidawave/prism
> prism info
(not to be confused with highlighting library prismjs and database toolkit prisma)
Quick start tutorial
Ultra efficient isomorphic. No JSON state, No rerender on hydration:
Prism compiles in getter functions for getting the state from the HTML markup. Events listeners are added with no need to rerender. The generated client side code is designed to work with existing HTML or elements generated at runtime. Virtualized state means that state can exist without being in the JS vm. When state is needed only then is it loaded into JS and cached for subsequent gets. This avoids the large JSON state blobs that exist on all other isomorphic solutions. This solution works for dynamic HTML. This should lead to smaller payloads and a faster time to interactive.
Server side rendering on non JS runtime:
For the server, Prism compiles components to ultra fast string concatenations avoiding the need for server side DOM. Prism can also compile string concatenation functions for Rust lang. See the Prism Hackernews Clone. This allows to write the markup once avoiding desync hydration issues and the time spent rewriting the render functions. It also acts as a checking step verifying correct HTML and type issues. Hopefully more backend languages in the future
Super small runtime:
Prism counter example compiles to 2kb (1kb gzip). According to webcomponents.dev this makes Prism the smallest framework. Of that bundle size 1.41kb is prism runtime library.
There is also the benefit that Prism does not need as JSON blob to do hydration on the client side. So for other frameworks, even if your bundle.js
is 10kb you may have another 6kb of preload data sent down with each request as well that needs to be parsed, loaded etc. With Prism the only JS that is needed is the bundle.
Web components authorization:
Prism compiles down to native web components. Prism takes HTML templates and compiles them into native DOM api calls. It takes event bindings and compiles in attaching event listeners. Prism can output single component definitions that can be shared and work natively. Building a app with Prism consists of batch component compilation and injecting a client side router to build a SPA.
Development:
Prism does not have any editor plugins. However association .prism
files to be interpreted as HTML works well as Prism is a extension of HTML. Although it does not provide full intellisense you get all the syntax highlighting and emmet.
"files.associations": {
"*.prism": "html"
}
Single file components and templating syntax:
Prism uses a similar style single file components to vue and svelte:
<template>
<h3>Counter</h3>
<h5 $title="count">Current count: {count}</h5>
<button @click="increment">Increment</button>
</template>
<script>
@Default({count: 0})
class CounterComponent extends Component<{count: number}> {
increment() {
this.data.count++;
}
}
</script>
<style>
h5 {
color: red;
}
</style>
Text interpolation is handled by inclosing any value inside {}
. To make a attribute dynamic it is prefixed with $
. For events the key is the name of the event prefixed with @
and the value points to the name of a method or function. In the following examples you will see a type argument sent to component which corresponds to the data type. This helps Prism with returning state from markup as the markup is all text and there may need to be numbers etc. It is also used by the reactivity binding framework for creating deep observables.
For importing components:
<template>
<h3>{postTitle}</h3>
...
</template>
<script>
// It is important that the class is exported
export class PostComponent extends Component<{postTitle: string}> {}
</script>
<template>
<PostComponent $data="post"></PostComponent>
</template>
<script>
import {PostComponent} from "./postComponent.prism";
...
</script>
For slots / sending children to components the <slot></slot>
component is used:
<template>
<div class="some-wrapper">
<!-- It is important that the slot is a single child -->
<slot></slot>
</div>
</template>
Conditionals rendering:
<template>
<!-- If with no else -->
<div #if="count > 5">Count greater than 5</div>
<!-- If with else -->
<div #if="count === 8">Count equal to 8</div>
<div #else>Count not equal to 8</div>
</template>
Iterating over arrays:
<template>
<ul #for="const x of myArray">
<li>{x}</li>
</ul>
</template>
For dynamic styles:
<template>
<h1 $style="color: userColor;">Hello World</h1>
</template>
<script>
interface IComponentXData {color: string}
class ComponentX extends Component<IComponentXData> {
setColor(color) {
this.data.userColor = color;
}
}
</script>
Client side routing
<template>
<h1>User {username}</h1>
</template>
<script>
@Page("/user/:username")
class ComponentX extends Component<{username: string}> {
// "username" matches the value of the parameter username specified in the url matcher:
load({username}) {
this.data.username = username;
}
}
</script>
Performing a client side routing call can be done directly on a anchor tag:
<template>
<!-- "relative" denotes to perform client side routing -->
<!-- href binding is done at runtime so href can be dynamic attribute -->
<a relative $href="`/user/${username}`">
</template>
or in javascript
await Router.goTo("/some/page");
There is also layouts which when the page is routed to will be inside of the layout. Layouts use the previous slot mechanics for position the page.
...
<script>
@Layout
export class MainLayout extends Component {}
</script>
...
<script>
import {MainLayout} from "./main-layout.prism"
@Page("/")
@UseLayout(MainLayout)
export class MainLayout extends Component {}
</script>
(Also note Layouts extends Components and can have a internal state)
Web components
Prism components extend the HTMLElement
class. This allows for several benefits provided by inbuilt browser functionality:
- Firing native events on component
- Reduced bundle size by relying on the browser apis for binding JS to elements
- Standard interface for data (with the hope of interop with other frameworks)
Web component compilation:
One of the problems of web component is that to issue a single component with a framework like React, Vue or Angular you also have to package the framework runtime with the component. This means if you implement a web component built with vue and another built with React into you plain js site you have a huge bundle size with two frameworks bundled. Web component are meant to be modular and lightweight which is not the case when 90% of the component is just framework runtime.
Prism attempts to move more information to build time so that the runtime is minimal. As it leaves reactivity to runtime it allows data changes to be reflected in the view. It also provides the ability to detect mutation so array methods like push
and pop
can be used.
Rust backend compilation:
As of 1.3.0 prism supports compiling server render functions to native rust functions. These functions are framework independent, fast string concatenations and strongly typed. Obviously transpiling between is incredibly difficult and while Prism can create its own Rust ast it can't really convert custom use code from TS to Rust. So there is a decorator that can be added to functions @useRustStatement
that will insert the value into the Rust module rather than the existing function definition. This code can do an import or as shown in the example below redefine the function:
<template>
<h1>{uppercase(x)}</h1>
</template>
<script>
@useRustStatement(`fn uppercase(string: String) -> String { return string.to_uppercase(); }`)
function uppercase(str: string) {
return str.toUpperCase();
}
@Globals(uppercase)
class SomeComponent extends Component<{x: string}> {}
</script>
Other decorators and methods:
<script>
@TagName("my-component") // Set components html tag name (else it will be generate automatically based of the name of the class)
@Default({count: 1, foo: "bar", arr: ["Hello World"]) // Set a default data for the component
@Page("*") // If the argument to page "*" it will match on all paths. Can be used as a not found page
@Globals(someFunc) // Calls to "someFunc" in the template are assumed to be outside the class and will not be prefixed
@ClientGlobals(user as IUser) // Variables global to client but not server
@Passive // Will not generate runtime bindings
@Title("Page X") // Title for the page
@Metadata({ description: "Description for page" }) // Metadata for server rendered pages
@Shadow // Use shadow DOM for component
class X extends Component<...> {
// Will fire on client side routing to component. Can get and load state
load(routerArgs) {}
// Will fire on the component connecting. Fires under connectedCallback();
connected() {}
// Will fire on the component disconnecting. Fires under disconnectedCallback();
disconnected() {}
}
</script>
Command line arguments:
Name: | Defaults: | Explanation: |
---|---|---|
minify | false | Whether to minify the output. This includes HTML, CSS and JS |
comments | false | Whether to include comments in bundle output |
componentPath | ./index.prism | (for compile-component ) Path to the component |
projectPath | ./views | (for compile-app ) The folder of .prism components |
assetPath | projectPath + /assets | The folder with assets to include |
outputPath | ./out | The folder to build scripts, stylesheets and other assets to |
serverOutputPath | outputPath + /server | The folder to write functions for rendering pages & components |
templatePath | template.html | The HTML shell to inject the application into |
context | isomorphic | Either client or isomorphic . Client applications will not have server functions and lack isomorphic functionality |
backendLanguage | js | Either "js", "ts" or "rust" |
buildTimings | false | Whether to log the compilation duration |
relativeBasePath | "/" | The index path the site is hosted under. Useful for GH pages etc |
clientSideRouting | true | Whether to do client side routing |
run | false | If true will run dev server on client side output |
disableEventElements | true | Adds disable to ssr elements with event handlers |
versioning | true | Adds a unique id onto the end of output resources for versioning reasons |
declarativeShadowDOM | false | Enables DSD for SSR the content of web components with shadow dom |
deno | false | Whether to add file extensions to the end of imports. For doing SSR |
bundleOutput | true | Whether to concatenate all modules together instead of later with a bundler |
outputTypeScript | false | Output client modules with TypeScript syntax (for doing client ts checking) |
includeCSSImports | false | Whether to include import "*.css" for components |
Assigning these settings is first done through reading in prism.config.json
in the current working directory. Then by looking at arguments after any commands. e.g.
prism compile-app --projectPath "./examples/pages" --run open
Assets:
Any files found under the assetPath
will be moved to the outputPath
along with the client style and script bundle. Any files in assetPath/scripts
or assetPath/styles
will be added the client side bundle.
License:
Licensed under MIT
Current drawbacks
- Prism and Chef is experimental and unstable
- Prism can only react to the data property on a component (no outside data)
- Prism only reacts to accessible properties on objects. This means types like mutating entries
Map
andSet
will not see those changes reflected in the frontend view