GithubHelp home page GithubHelp logo

kaleidawave / prism Goto Github PK

View Code? Open in Web Editor NEW
110.0 4.0 1.0 703 KB

(No longer in development). Experimental compiler for building isomorphic web applications with web components.

License: MIT License

JavaScript 0.22% TypeScript 99.74% HTML 0.03%
isomorphic-javascript webcomponents server-side-rendering compiler typescript experimental hydration

prism's Introduction

Prism Compiler

Twitter Issues Stars On NPM Node Version

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)

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 and Set will not see those changes reflected in the frontend view

prism's People

Contributors

kaleidawave avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

ink-splatters

prism's Issues

SSR data parameter of layouts

Currently a layout component (Denoted with @Layout decorator) can have a state on the frontend. It inherits from Component and its markup is subject to the same templating binding and reactivity that any other component is subject to.

The problem arises around server side rendering pages that use a layout which has data. The component should append its inherited layout parameter to its own parameters.

Issue is around:

// TODO layout data is different to component data. Should be interpreted in same way as client global

There is also a opportunity here to refactor the way client global parameters work in the same way.

Lookup of non server rendered data

(not really a issue but something tools that explicitly send down the entire state as an object don't encounter)

So given this template:

<template>
    <div #if="someX">
        <h1>{someY}</h1>
    </div>
</template>

If the template is rendered with the state of someX === false the h1 below will not be rendered and sent to the client. This is a little bit of a problem as if this.data.someY is called it will be null but when the server rendered it may have had a value.

Two fixes for if it is important that someY needs to be accessed at runtime:

1: Use $hidden:
<template>
    <div $hidden="someX">
        <h1>{someY}</h1>
    </div>
</template>

This will still be sent to the client and accessible from the DOM but have the effect of not being visible by the user. It someX is very likely to change at runtime this is a good way to go as it can be instantly revealed rather than the tree being generated on the client. However if someX is hardly ever changed or never changed and the tree under it is large it will end up with a lot of extraneous html.

2: Put the value elsewhere:
<template>
    <div $data-some-y="someY"></div>
    <div $hidden="someX">
        <h1>{someY}</h1>
    </div>
</template>

This way someY is still sent to the client un conditionally. Bad if someX is often truthy and someY is sent twice, especially is someY is large.

Pass single props to components

Currently to send data to a object the only supported method is through sending a whole already existing object to a component:

<SomeComponent $data="someObj"></SomeComponent>

This works fine through csr, ssr and cssu (client side state updates).

It would be nice if rather than having to send a whole object that it could be constructed from state:

<SomeComponent $data="{someProp: x}"></SomeComponent>
<SomeComponent $data="{...someObj, someProp: x}"></SomeComponent>

This should currently work with csr and ssr. It just takes a few additions to the set and get compiler logic to work. But there may be some nuances with reference types...?

There is then the opportunity to add more intuitive syntax for this:

<SomeComponent $data-someProp="x"></SomeComponent>
<!-- To be desugared to: -->
<SomeComponent $data="{someProp: x}"></SomeComponent>

Allow page to match on multiple route patterns

It would be cool if the @Page decorator could have multiple arguments for each URL pattern it matches on.

e.g.

@Page("*", "/error")
@Page("/upload", "/upload-post")

Should be quite simple, just repeat the entry on Router.routes with a different regexp

Reassignment after rendering conditional element / array item rendered twice

Given the following template:

<template>
    <div #if="someString !== 'x'">
        {someString}
    </div>
</template>

Let someString equal 'x' initially.

If someString is reassigned to "abc" then at runtime:

  1. someString !== 'x' is now truthy so the div is rendered with the state (this.data equal to) {someString: "abc"} and swapped in for a placeholder element
  2. someString value has changed so it tries to set the first text node of the div (if it exists) content to "abc"

The problem is when the render of the div occurs it will render in with the updated state so it will have "abc" as its text content. Therefore the second call is redundant for this update. However a second assign to someString of "xyz" requires the second statement, as the update would not be reflected on the view as the render of the div and swap would not occur.

This isn't a big deal here, no bad effects only additional calls with no effect which could be a perf problem.

However for:

<template>
    <div #if="someArr.length > 3">
        <ul #for="const x of someArr">
             <li>{x}</li>
        </ul>
    </div>
</template>

.pushing to someArr while someArr.length === 3 will cause the argument of push to be in the ul list twice. Once from initial render when someArr.length > 3 becomes truthy and second when the push call means assigning to a index outside of current length causing a new li to be rendered.

There are some mitigations such as maintaining a separate prop for the condition as not to clash with variables used inside and controlling that prop manually.

But there is definitely some ways the compiler could see this problem occurring and generate code for mitigations.

Compile server functions to other backend languages

Currently Prism compiles functions for rendering components and pages. These functions are built up of string concatenations so are incredibly simple and fast. These also feature string escaping etc... But they are very generic and simple to create as most of the work is done during templating.

But Prism only compiles these functions for JS engine use. Many other server side rendered sites use other backend languages such as golang, rust, ruby, python, c#, c++, ... . Which is a problem for building isomorphic sites as manually adding ssr templating desyncs the process of client side and server side views and templating. There are currently ways of calling JS code in golang and such and some have used that to achieve ssr of react apps. But as Prism is a compiler and there is not lot of complexity in the render functions I don't think it would be to hard to add the ability to output native golang, rust, ruby... functions for use with a backend framework.

This would be beneficial:

  • Enable isomorphic rendering for other languages that are used because they are the language currently used for a codebase or have packages that do not exist in the JS ecosystem
  • Enable isomorphic rendering for more performant languages to enable super fast ssr and reducing server latency and strain

Three current considerations:

  • Some languages have there own templating library/syntax would Prism compile to those? Golang has its own html templating language and C# has .cshtml. These seem to exist for DX, ease of use and compile time checking. However Prism offers all of this during compilation so they are not needed and add additional nuance. And prism output is designed to be run rather than analysed.
  • There is more that just syntax in the difference between programming languages This is very true. Functions, imports, string, concatenation and objects/structs exists in nearly every languages but actual runtime functions vary significantly in naming and existence. For example Intl.NumberFormat class does not exist in any other language in form or name. So sending it straight over to python would not work.

But there could be something like:

@returnValueIfUnderNonJSBackendLanguage(`f"{x:,.3f}"`)
function formatNumber(x: number) {
    return new Intl.NumberFormat('en').format(x)
]

That during ssr compile time could do some switching of the return expression ...

  • Interfaces / type definitions Currently if you build a Prism application with backendLanguage as ts it will include all the type information in the .ts server render function. (this works becomes of some specific things in chef and the fact ts is a superset of js. the ts info exists in the server module ast but is never rendered out...). It should be quite simple to transpile the interface ast (struct for golang and rust). Howerver actual data types do not exist across languages. This should be simple for some things like arrays. However types like number do not exactly map over. Whereas number exists in ecmascript, in rust there are u8, u16, i8, f64, .... and none of the quite match up. I think for this case it would be assume f64 and then you might have to do some casting when using this function. This problem is similar to the last point.

This does sound quite complex to implement but I think it would be quite simple. Just implement a fraction of syntax into chef (no parsing just render() methods) and add some step for transpilation. There would need to be a little work to implement the structure to build to non js based languages but once that is done then I think adding support for a language would be simple (especially after the first language support is implemented).

Implementing this would follow a common development characteristic of Prism where implement support for basic templating and add more complex templating later.

@onGet and @onSet custom hooks

Currently there several types of bindings:

export enum ValueAspect {
Attribute, // Affects a specific attribute of a node
Data, // A components data
InnerText, // Affects the inner text value of a node
Iterator, // Affects the number of a children under a node / iterator
Conditional, // Affects if a node is rendered TODO not visible but exists
DocumentTitle, // Affects the document title
SetHook, // Hook to method on a class
Style // A css style
}

Currently set hook is not implemented. It would consist of adding a @onSet decorator to methods in the component class definition that would fire when the value parsed in as a argument is updated (the same way it works with updating the view via the DOM)

Example usage:

class SomeComponent extends Component<{title: string}> {
    @onSet(title)
    titleUpdate(value) {
        console.log("Title has updated to", value)
    }
}

Where value is the new updated value.

There could also be an opportunity for a @onGet decorator which would decorate a method that would return a value for manual server hydration. This could be used in places where Prism analysis for hydration fails. This would not be the same as a computed property. Once the hydration is needed and the @onGet method is fired and returns a value to the state then further gets to the property would return that in state rather than call the @onGet method again

Remove unused runtime library for component compilation

There is currently a bunch of code used at runtime that needs to be bundled for runtime to work. This includes things like the createObservableObject, the Component that every component extends and several other functions used for doing common runtime operations.

Runtime currently sits at around 4.34kb minified.

This is okay but there there are many large functions and methods that are only needed for #for and #if expressions. e.g createObservableArray.

This is okay for full apps build under prism compile-app that are likely to need all these features but for components built with prism compile-component that don't use #for or #if they may not be needed and will be more expensive to distribute.

I propose a model where under compile-component it sends a object to the construction of the component that if there are iterator and conditional expressions then will a flick a boolean switch. If switches are not true then some functions will be filtered out of the prism runtime bundle.

For very simple components this could reduce bundle by 1/2.

This is drastically more simple approach to tree shaking which works through code resolving references and automatically cut things out. Currently decent tree shaking is out of scope for chef

Recursive components

Current you can use a component in a template with:

<template>
    <SomeComponent></SomeComponent>
</template>

<script>
    import {SomeComponent} from  "../../someComponent.prism";
   
    ...
</script>

And this works as long as SomeComponent is exported (prefixed with export keyword)

But there is no way of using the current component in a recursive way.

Using This could be a way of referring to the current component:

<template>
    <div #if="escapeLoop">
          <This></This>
    </div>
</template>

This should work as during client side as it will do this.append(document.createElement(**this component tag**) which is valid. It should work during server side rendering as it will do call the currently invoked function just like standard recursion

Just remember to add a exit condition ๐Ÿ˜‹

Value linked to object prop does not update if object is passed to another component

Given:

<SomeComponent data="someObj"></SomeComponent>
<h3>{someObj.username}</h3>

Executing:

***.data.someObj.username = "lorem-ipsum"

SomeComponent will see the updates and update its view but the text content of the h3 will stay the same.. This is because the component data tree references SomeComponents reactive data property under someObj and that reactive data tree is isolated to SomeComponent so it cannot bubble up changes to the main component.

This is quite a tricky one and one that prevents some uses cases. For now one could collapse SomeComponent into the others dom:

<div>
    <!--. .. -->
    <h2>{someObj.username}</h2>
    <!--. .. -->
</div>
<h3>{someObj.username}</h3>

To fix this there would likely be changes to observable.ts and the data reactivity compilation. It would have to know that the object prop is used twice, one in its own component DOM and also passed to another component. It could then wrap / proxy it and make sure the changes are done both in its own DOM and passing the update down to SomeComponent.

This would still have the issue that if SomeComponent changes its own username from inside, whether this should bubble up to the parent importer. This is not possible in React and why their context library. Not sure whether it would be a good feature but certainly possible under compilation.

There is also ways that #16 could help with this. Such as manual firing a method that could emit a event to the parent (importee) component so that it could update the h3 value...

Bundle imports

Current imports are used for:

  • Importing Prism components so that templating is aware of custom tags
  • Importing @Globals for server
  • import type for the type resolving system

As of v1.0.2 Prism strips all imports and exports when bundling the whole application. This means that if you are importing a es module from another server then it will be removed.

Bundling is currently done via concatenating all <script>s in .prism definitions and .js and .ts files in the scripts folder of the assets path. Rather than via imports.

The stripping of imports and exports is done because effective code splitting is not quite worked out yet. And it over compensates by removing all imports as a bad static import would cause the script to not run.

And v1.0.2 is not a production build meant to integrate with other code. If importing is important then you can get away with dynamic imports.

Imports bundling and tree shaking are a bit out of the scope for chef rn so it would mean bringing webpack, rollup or other bundlers into the toolchain.

Unnecessary expression construction

The static method Expression.fromTokens is used as the path for all "value" parsing. The fromTokens method will return a ValueTypes rather than a Expression

static fromTokens(reader: TokenReader<JSToken>, precedence = 0): ValueTypes {

Currently if there is a field in something like array literal, right hand side of assignment or if condition Expression.fromTokens is called.

This is okay but the fromTokens originally was for just expressions and there is a relic left in from that in which a Expression is eagerly created/constructed but then may be never used and dropped.

//@ts-ignore TODO expression requires parameters
const expression = new Expression({ lhs: null, operation: null });

return expression.operation === null ? expression.lhs : expression;

Expression should be constructed later if it encounters a operation and never if it is just a variable or literal

Fixing this would also get rid of the temp //ts-ignore with the invalid expression constructor arguments

I think Expression.fromTokens can stay as the entry for "value" parsing but there could be instead parseValue in the same way there is parseStatement...? ๐Ÿค”

As this is a very hot path this is important performance issue

Implement intersection types

On trying to parse a type signature with a intersection type it throws ParseError: Expected "End of script" received "&".

Example:

const bDeclaration = VariableDeclaration.fromString(`let b: a & b;`);

Render out enum under JavaScript

Currently rendering out any Enum with settings.scriptLanguage === ScriptLanguages.Javascript will throw:

// TODO render javascript as object literal possibly?
throw new Error("Method not implemented.");

Chef should instead create a temp object literal and render that out instead if rendering out Javascript. Object should preferable be frozen to prevent runtime mutation. I think this is also the tsc implementation.

This wouldn't fix const enums as they require more that just rendering additional js

Expression reversal

The idea behind Prism JIT hydration system is that as a compiler it knows where variables are interpolated and so can build ways to retrieve them and cache them into the components data / state.

The first step is the runtime knowing where in the DOM tree they are interpolated. Prism does this by adding identifiers to nodes and also what part they are interpolated at (attribute, text node etc). The compiler builds in these expressions in get-value.ts

And this is fine for simple templates:

<template>
    <div $data-x="varX"></div>
</template>

Compiler output:

getVarX() {
   return this.getElem(<prism generated id>).getAttribute("data-x")
}

The problem arises around more complex templates:

<template>
    <h1>
        {varX * 2}
    </h1>
</template>

Prism recognises that rendered output may not be 1:1 with rendered markup and there is a case for doing manipulation during templating. The problem is that unlike the previous scenario returning the text node data as a value for varX would incorrectly hydrate varX as being twice the value it is. A step is required to transform rendered output to original state through a reverse of what was done during interpolation.

Currently Prism has reversers for multiplication, division and template literals interpolation: reverse.ts

varX * 2 will divide the text content (relies on string casting to number) by 2. Division does the opposite. For simple template literals the following `Hello ${name}` will convert it into a slice expression.

This is an issue bigger than Prism:

For example attempting to produce a reverse expression for a + b (where a & b are numbers) is impossible. The rendered output may be 15 which could come from a = 3, b = 12 or a = 5, b = 10 etc.

There are other issues around text concatenation with multiple variables (Only for attributes, text node concatenation is fine as it is split up with comments). For example: `${x}a${y}`, if its rendered value is aaa then it is ambiguous whether the cases are x = "aa", y = "", x = "", y = "aa", or x = "a", y = "a".

There are some other very technical scenarios here around what if the x value is only hydrated to test if it begins with "b" or something where the ambiguity is okay. And there is a possibility that if you are ok with ambiguity there could be some regex here.

This is a common case around number and date formatting where the literal value is not there and it requires parsing to get the rendered version back to something looking like it was on the server.

Where this issue becomes very difficult is around non 1:1 mappings and expressions that involve more than one variable.

For now this can be solved by server rendering the data to $data-* attributes so the client has easy access to values. This will make payloads bigger unfortunately. (There is another issue here around not client side rendering these attributes or doing updates to them).

Types:

The other issue is that server nodes and attributes always return strings. This is a problem for properties than are numbers or Dates and at client runtime need to be of that type. This is one of the reasons for the generic data argument on Component. The type information parsed at compile time and will inject parseFloat or new Date() to properties that are of that type.

Text interpolation reactivity throws when under un rendered element

Given the following template:

<template>
    <div #if="someX">
        {someY}
    </div>
</template>

Attempting to set someY while someX is falsy will throw as the div is not rendered and thus *div*.childNodes[0].data is undefined and cannot be assigned to.

The compile is aware the element "nullable" and thus should generate code that checks can do update before trying to assign to undefined and throwing.

There is a lot of edge cases and other issues around "nullable" elements and there may be a better solution to this later but for now a patch to prevent the throw and the disruption to later execution is required

Use chrono for Date with Rust SSR

If a component has a Date property then import the chrono module and use chrono::DateTime for properties of type Date. chrono::DateTime::to_string seems to return a string which can be successfully parsed by the JS Date constructor so hydration of the raw output should work just fine.

Progressive server side rendering

Prism SSR functions will only return once the whole page/content has been string concatenated. This is okay but a technique known as progressive render improves on this process by yielding chunks back that can be written / streamed to the response. Using Transfer-Encoding: chunked in the response header means the client will gradually render in content before the server has ended the response.

These would mean ditching the current template literal approach and instead using yield with a generator function. And there would need to be some figuring out to where to draw the end of chunks while still being correctly parsable by the client.

The other thing is that progressive render is to get content to client before all the data / state has been fully formed on the server rather than to send content before the full concatenation is done. aka progressive render is for dealing with the speed issue of getting data for the view (e.g. accessing and making db requests) rather than the time it takes to do the string concatenation (which is already fast).

This complicates things as rather than sending a fully formed data structure to the render function it requires some sort of partial unresolved state. This can be done is JavaScript with getters, promises and generators but I am unsure of how it would work out in Rust (#19).

Anchor tag point to page

Currently to do client side routing you must manually point to a URL and add the relative attribute.

It would better if rather than having to point at a URL that you could point at a page component instead.

<template>
    <a #to="SomeOtherPage">Go to SomeOtherPage</a>
</template>

<script>
     import {SomeOtherPage} from "./someOtherPage.prism";
     class SomePage extends Component {}
</script>

At compile time this would transform the #to to a href and add the client side routing events.

For pages with dynamic URLs e.g. /some/path/:x pointing to the page would a function call e.g.: #to="PathWithDynamicPath(this.data.x)"

This would strongly bind URLs to pages and remove the lookup/guesswork with hooking up URLs to pages. Importing the component would be necessary.

Should be fairly easy to implement. There may be some problems with cyclic #tos. Also pointing to the current page but under a different URL argument would require #21

Language parsing breaks with tabs

Currently the tab characters (\t) are considered a ident token and so breaks during building up the AST structure. Needs fixing to consider the tab character the same as whitespace ๐Ÿ”ง

Type resolver does not recognise enums

Given a enum in the module trying to resolve the properties of that type throws a error: Error: Could not find type: X in module

const mod = Module.fromString(`enum X { A, B }`);
console.log(typeSignatureToIType(new TypeSignature("X"), mod));

typeSignatureToIType should return enums to be of type number (or string). Think that is good enough

Make compiler work on the browser

The several parts of the compiler that are locked to node runtime. Hopefully with some changes the compiler can span multiple contexts.

The are several parts of internal that were written quickly to get things working but now need to go back over to improve stabilty.

  • Isolate settings. Several parts rely on the global state of settings. This improve compiling multiple prism projects at once
  • Extend HTMLElement properties with a weak map rather than assigning to object (may have speed improvements)
  • Figure how to import runtime without fs (fetch???)
  • Figure how to configure TypeScript to target web

Build out full server (not just functions)

Currently Prism only outputs functions. And they can be imported so they can be used with a express or any other backend js framework to render body. e.g:

import {renderMainPage} from "../somePrismOutput/main-page.prism"

const app = express()

app.get("/", (req, res) => {
    res.send(renderMainPage(yourData))
});

This is great, you get the benefits of prism automating of synchronisation of client side render functions and server side render functions.

But Prism can do a little more here. It already knows what url routes to which pages. And its a compiler so its really good at generating code. So Prism could generate a whole express or oak application.

This could be implemented through a setting:

generateServer: "express" | "oak" | null

and the server runtime settings (port, hostname, ...) would be pulled in at runtime through process.env (or Deno.env.get(*)) or do defaults. It would compile in load callbacks of page components.

This would make it really simple to spin up a isomorphic project with Prism really quickly.

There is a bunch of things that would not be implemented. Things such as cookies, headers, separate server specific load functions and others. ands that's fine as this feature is not for building complex sites. It is just for getting a Prism app working on the server without having to manually build an express app to do so.

Literal types being interpreted as actual type

Currently chef interprets literal types as being actual types.

> const bDeclaration = VariableDeclaration.fromString(`const b: "abc" = "abc";`);
> bDeclartion
> TypeSignature { name: "abc" }

Which causes them to be rendered without quotations:

> bDeclaration.render(getSettings({scriptLanguage: ScriptLanguages.Typescript}))
> const b: abc = "abc";

Literal types (strings, numbers and booleans) should be recognised in TypeSignature as a Value and parsed accordingly

Raw html injection

Currently all interpolated text nodes and attributes are escaped. This is done on the server through a function call and on the client by assigning to character data which will not create DOM nodes.

But there are cases for inject raw html. There are ways around it but they get complex and likely to complex for Prism reactivity system. If they are sanitized then it should be fairly safe to do raw.

There could be a new attribute #html which depicts the elements innerHTML is the value of the attribute

<template>
    <div #html="`<h1>Hello World</h1>`"></div>
</template>

Any element with #html must have no children.

This should be fairly easy to do. Add a new property to NodeData depicting element uses raw HTML. For client side rendering add the property innerHTML: **#html value** to the attribute object. For server side rendering drop the wrapping escape call. Then there is the reactivity which would point to .innerHTML rather than .childNodes

Element retrieval using indexes rather than identifiers

Currently elements that need to be retrieved at runtime (for events and bindings) are given a identifier at compile time. Subsequent code gen uses that identifier for building expressions that refer to that element. The identifier starts with p and then several random characters (identifiers are checked across components not to collide although it is not guaranteed across foreign components). For example the following button will get a class with something like p1234:

<template>
    <button @click="alert">{someText}</button>
</template>
..

After analysis the button will become something like <button class="p1234">..</button>

This is so that during hydration adding the event listener is as simple as this.querySelector(".p1234").addEventListener(..) or during runtime if the value of someText is altered then this.querySelector(".p1234").innerText = ... There is also a Map on the component instance which caches them so repeated accesses don't go looking for elements again. This abstraction is through the method Component.getElem

This was a simple early implementation in Prism but there are some issues with this:

  • Additional payload during server side rendering for all the class names.
  • querySelector performance concerns for large trees
  • Issues around elements non unique elements in for loops
  • Caching is mem expensive and sometimes unnecessary. Map is not created lazily

A solution:

Instead of adding identifiers and generating lookups via identifiers, elements could be retrieved through getting their index in the tree.

<template>
    <div>
        <div>
            <button @click="alert">{someText}</button>
        </div>
    </div>
</template>

Get element by index:

const button = this.children[0].children[0].children[0]

The indexes can be calculated at build time. This is already the process for referencing elements in for loops.

Things to consider:

  • Adding something like getElement(0, 0, 0) could abstract across the repeated .children[x] chaining
  • For elements in loops rather than a static number it would be a variable e.g. getElement(0, currentIndex, 0)
  • children vs childNodes as the former doesn't take into account text nodes
  • Compare performance
  • Is element caching necessary?
  • Fragments don't exist and "for"s are done under single elements so elements indexes should be reasonably fixed
  • Adding a children getter to components with slots as identifiers are used

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.