Bundle

The bundle file is interpreted by the server and is used to create routes, manage assets, styles, a few other things. This article will cover all these aspects.

When a project is initialized with lvweb init, the bundle file will be generated in the bundle directory of the project (bundle/bundle.lv). The generated bundle file will have a home ViewRoute and a main Stylesheet:

import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import .app

instance bundle Bundle{
    ViewRoute{ url: '/' c: Home }
    Stylesheet{ src: './styles/style.css' output: 'style.css' }
}

When running lvweb serve, the bundle file is sent to the server, where the route and stylesheet are prepared, optimized and then served to the client. ViewRoute uses webpack behind the scenes to bundle all the files for that specific route. ViewRoute is one type of route for views, but there are other route types available like the GetRoute or PostRoute, which will be covered in the following section.

Routes

GetRoute

GetRoute is used to define an endpoint for GET requests. The component is built on top of express api, so the implementation for the request handler is similar to the one used in express. This is how to use it in a bundle file:

instance bundle Bundle{
    GetRoute{ 
        url: '/get-route'
        f: (req, res) => {
            res.send('hello from get route')
        }
    }
}

The req and res objects are forwarded from express, so they have the same API and functionality.

GetRoute also has a middleware property, which is forwarded to the middleware parameter in express:

GetRoute{ 
    url: '/get-route'
    middleware: (req, res, next) {
        console.log('passed by middleware')
        next()
    }
    f: (req, res) => {
        res.send('hello from get route')
    }
}

Route paths and parameters are supported as well:

// match acd and abcd
GetRoute{ url: '/ab?cd' f: (req, res) => { res.send('ab?cd') }}
// match abcd, abbcd, abbbcd, and so on
GetRoute{ url: '/ab+cd' f: (req, res) => { res.send('ab+cd') }}

// match route parameters
GetRoute{ url: '/users/:userId/books/:bookId' f: (req, res) => { res.send(req.params) }}

To summarize, the following are the properties for GetRoute:

  • url:string: The url of the route
  • middleware:Array|function: Middleware handler(s).
  • f:function: Route handler.

PostRoute

The PostRoute is used for post requests:

PostRoute{ 
    url: '/post-route'
    f: (req, res) => {
        res.json(req.body)
    }
}

The properties are the same as for the GetRoute :

  • url:string: The url of the route
  • middleware:Array|function: Middleware handler(s).
  • f:function: Route handler.

ViewRoute

The ViewRoute is stricter than get or post routes. Instead of the handler function, it specifies a component to load either on the client side or on the server side, depending on the render property. The component must inherit either from PageView or Application:

ViewRoute{ url: '/' c: Home } // client side render
ViewRoute{ url: '/' c: Home render: ViewRoute.CSR } // client side render
ViewRoute{ url: '/' c: Home render: ViewRoute.SSR } // server side render

When rendering on the client side, view components should be declared in separate files, outside of any server imports, as the files will be bundled together for the client.

Note: When running lvweb serve all routes will be rendered on the client, including routes with SSR rendering. This is due to the serve command running in development mode, as rendering on the client is easier to debug, since it supports live reloading. Compiling (lvweb compile) and running in production (lvweb run) will render these routes on the server side. There's a way to test out server rendered routes in development, by using --renderMode production args in lvweb serve:

lvweb serve --renderMode production

The following properties are available for ViewRoute:

  • url:string: The url of the route
  • middleware:Array|function: Middleware handler(s).
  • c:component: Component to load on the client side.
  • render:ViewRoute.SSR/ViewRoute.CSR: Server or client side rendering.
  • placement:Page|ViewPlacement: Layout placement. See pages or layouts section in this article for more info.

Nesting Routes

Routes can be nested inside a Route component. The GetRoute below will be available at /nested/get-route:

instance bundle Bundle{
    Route{
        url: '/nested'
        GetRoute{ 
            url: '/get-route'
            f: (req, res) => {
                res.send('in /nested/get-route')
            }
        }
    }
}

Route components support middleware that will be executed when any nested route gets requested:

instance bundle Bundle{
    Route{
        url: '/nested'
        middleware: (req, res, next) {
            console.log('passed by main route middleware')
            next()
        }
        GetRoute{ 
            url: '/get-route'
            f: (req, res) => {
                res.send('in /nested/get-route')
            }
        }
    }
}

Assets

Assets are files that are served to the client. Bundles provide 3 components to manage assets: BundleFile, BundleDirectory and Stylesheet.

Files

BundleFile is used to serve a single file at a specific route. When compiling the bundle, the file will be copied in the dist directory.

instance bundle Bundle{
    BundleFile{ src: './assets/images/background.jpg' output: '/images/background.jpg' }
    BundleFile{ src: 'images-package/image.jpg' output: '/images/image.jpg' }
}

BundleFile has 2 properties:

  • src:string: Specifies the source location for the asset, or in other words, the location where to serve the assset from.The location can be either relative to the root of the bundle package (./assets/images/background.jpg), or inside a package found in node_modules. For example, in case of a package-based url (images-package/image.jpg), the server will look for the images-package similar to how a javascript import would be searched for. In this example, the package might be found in node_modules/images-package/image.jpg.
  • output::string: The url the asset will be available at.

BundleFile can also be used to serve Javascript files.

instance bundle Bundle{
    BundleFile{ src: 'bootstrap/dist/js/bootstrap.bundle.min.js' output: '/js/bootstrap.bundle.min.js' }
    BundleFile{ src: 'bootstrap/dist/js/bootstrap.bundle.min.js.map' output: '/js/bootstrap.bundle.min.js.map' }
}

Directories

Directories can be served with BundleDirectory:

instance bundle Bundle{
    BundleDirectory{ src: './assets/images' output: '/images' }
    BundleDirectory{ src: 'images-package/images' output: '/images-package' }
}

BundleDirectory has the same 2 properties as BundleFile:

  • src:string: Specifies the source location for the directory. This will be scanned recursively. The location can be either relative to the root of the package (./assets/images), or inside a package to be searched for (images-package/images).
  • output::string: The url the folder files will be available at.

Stylesheets

The Stylesheet component is used to declare stylesheets the same as way as bundle files:

Stylesheet{ src: './styles/style.css' output: 'style.css'  }

Stylesheet adds 2 more features.

Stylesheets can be merged:

Stylesheet{ src: './styles/header.css' output: 'style.css' }  // create style.css
Stylesheet{ src: './styles/content.css' output: 'style.css' } // merge in style.css

And stylesheets can be processed:

Stylesheet{
    src: './styles/style.css'
    output: 'style.css' 
    process: PostCSS.create(TailwindCSS.createForPostCSS()) 
}

The example creates a PostCSS processing function with TailwindCSS plugin.

Custom processing functions can be implemented. See this code on how PostCSS.processis implemented:

import postcss from 'postcss'
import url from 'postcss-url'

component PostCSS{

    Array plugins: []

    static fn create(plugins:Array){
        const postcss = PostCSS{}
        postcss.plugins = plugins ? plugins instanceof Array ? plugins : [plugins] : []
        return postcss
    }

    static fn createUrlPlugin(opts:Object){
        return url(opts)
    }

    fn process(file:string, content:string, destination:string){
        return postcss(this.plugins)
            .process(content, { from: file, to: destination })
            .then(result => {
                return { content: result.css, map: result.map ? result.map.toString() : null }
            })
    }

}

To summarize the 3 properties from Stylesheet:

  • src:string: style source location. This can be either relative to the root of the package, or inside an installed package.
  • output::string: The url the file will be available at.
  • process::element: The style processing element.

Pages

All view routes are rendered on pages. For client side rendering routes, their page is loaded on the server side, while their content is then loaded on the client.

Pages are useful for 2 things:

  • In case of client side rendering, have partial content delivered on the server before rendering the rest on the client
  • Create a predefined common layout shared between multiple ViewRoute's

A ViewRoute can specify the page it will render to by its placement property.

A bundle provides a default IndexPage, so ViewRoute's' without a placement property set will simply render on that page:

import live-web.dom
import .page

component IndexPage < Page{
    output: 'index.html'
    title: 'Index'
    clientRender: body

    Body{
        id: body
    }
}

The page configures the output property, and since it's index.html, the server knows it's the default page. The title is the default title of the page in case it doesn't get overriden by the current PageView. clientRender is the actual location where the view will be rendered, in this case the page body. We can also render deeper inside a page hierarchy, for example:

import live-web.dom
import .page

component IndexPage < Page{
    output: 'index.html'
    title: 'Index'
    clientRender: content

    Body{
        Menu{ /* ... */ } 
        Section{
          id: content
        }
    }
}

This will render inside the Section element.

To create a custom page, simply create a page component inside your project:

import live-web.dom
import live-elements-web-server.page

component CustomPage < Page{
    output: 'custom.html'
    title: 'My Custom Page'
    clientRender: content

    Body{
        H1`Custom Page`
        Div{
          id: content
        }
    }
}

And then add it to the bundle, and place a ViewRoute to be rendered on that page:

import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import .app

instance bundle Bundle{

    // create the page in the bundle
    CustomPage{ id: customPage } 
    // assign the page to the placement property
    ViewRoute{ url: '/' c: Home placement: customPage } 

    Stylesheet{ src: './styles/style.css' output: 'style.css' }
}

You will now see both the title from the page, and the content of the ViewRoute.

Layouts

On top of pages, layouts provide additional customization for ViewRoute placement. Layouts are defined using the ViewPlacement component. ViewPlacement's sit between pages and ViewRoute's, in order to share commong layouts.

For example, a page can share a layout with multiple views, a ViewPlacement can define a menu for some, and another ViewPlacement can add a sidebar for others:

import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import live-elements-web-server.view
import .app

instance bundle Bundle{

    CustomPage{ id: customPage }

    // create a MenuLayout inside CustomPage
    ViewPlacement{ id: menuLayout c: MenuLayout placement: customPage } 
    // place the Home view inside the menuLayout
    ViewRoute{ url: '/' c: Home placement: menuLayout }

    // create a SidebarLayout inside CustomPage
    ViewPlacement{ id: sidebarLayout c: SidebarLayout placement: customPage }
    // place the same Home view inside the sidebar layout
    ViewRoute{ url: '/with-sidebar' c: Home placement: sidebarLayout }

    Stylesheet{ src: './styles/style.css' output: 'style.css' }
}

A ViewPlacement component inherits ViewLayout, and has a similar structure to a ViewPage:

import live-elements-web-server.view
import live-web.dom
import live-web.behavior

component MenuLayout < ViewLayout{
    id: layout
    render: content

    Div{
        Div{ classes: ['menu']
            H2`Menu`
        }
        Div{ id: content            
        }
    }
}