Bundling in Nui
Nui uses parcel under the hood to bundle everything together. The regular cases try to inline everything and place them in a single index.html, sometimes this approach is not possible or desireable.
This is why there is the option to register custom schemes, which allow you to handle requests to a custom scheme, allowing you to serve files from a folder or even generate them on the fly.
Status Quo in the Template
The template provides the following index.html file. In this index.html file, sources and styles are inlined by parcel by using the import statements instead of adding every file by using script tags.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nui</title>
<!-- You have to import like this to achieve inlining of the style -->
<style>
@import "./styles/main.css";
</style>
<!-- You have to import like this to achieve inlining of the script -->
<script type="module" defer>
import "./source/index.js";
import "../bin/index.js";
</script>
</head>
<body>
</body>
This will produce as single index.html located at */build/module-TARGETNAME/bin/index.html
which is then automtically accessible from the backend source by including it:
// This file is generated by nui.
#include <index.hpp>
int main() {
using namespace Nui;
Window window{"Window Title", true /* may open debug window */};
// index function to be used here:
window.setHtml(index());
}
Inline
Image assets
Images can be inlined in the CSS using:
.foo {
background: url(data-url:./background.png);
}
Typescript/Javascript Code
Just import regularly from your source files:
import _ from "lodash";
Or from index.html:
<script type="module" defer>
import "./source/bla.js";
</script>
External
Sometimes you dont want to bundle because resources would become to big for a big bundled index blob.
There are 2 ways to achieve this:
- Custom schemes
- Virtual host name to folder mapping
Prefer manual custom schemes over folder mapping!
Custom Schemes
Custom schemes allow you to define a custom url scheme, which are than handled by a function passed by you. To use a custom scheme, you have to register it in the backend:
#include <nui/core.hpp>
#include <nui/rpc.hpp>
#include <nui/window.hpp>
#include <roar/mime_type.hpp>
#include <iostream>
#include <filesystem>
#include <fstream>
// This file is generated by nui.
#include <index.hpp>
int main(int, char** argv)
{
using namespace Nui;
using namespace std::string_literals;
std::filesystem::path programDir = std::filesystem::path(argv[0]).parent_path();
CustomScheme scheme{
.scheme = "nui"s,
.allowedOrigins = {"*"s},
.onRequest =
[programDir](CustomSchemeRequest const& request) {
std::cout << "Request: " << request.scheme << std::endl;
std::cout << "URI: " << request.uri << std::endl;
std::cout << "Method: " << request.method << std::endl;
std::cout << "Path: " << request.parseUrl()->pathAsString() << std::endl;
std::cout << "Headers: " << std::endl;
for (auto const& [key, value] : request.headers)
std::cout << " " << key << ": " << value << std::endl;
// Currently there is no streaming way to obtain the body, if its large.
// Cannot read a body on linux with webkit 2.39 and lower.
std::cout << "Body: " << request.getContent() << std::endl;
// make path relative to / to avoid directory traversal
const auto file =
programDir / "assets" / std::filesystem::relative(request.parseUrl()->pathAsString(), "/");
// Check if file exists and return 404 if not
if (!std::filesystem::exists(file))
{
return CustomSchemeResponse{
.statusCode = 404,
.reasonPhrase = "Not Found",
.headers =
{
{"Content-Type"s, "text/plain"s},
// Do not forget to allow CORS
{"Access-Control-Allow-Origin"s, "*"s},
},
.body = "Not Found",
};
}
// Read file
std::ifstream reader{file, std::ios::binary};
reader.seekg(0, std::ios::end);
std::string content(reader.tellg(), '\0');
reader.seekg(0, std::ios::beg);
reader.read(&content[0], content.size());
// Get mime type
const std::string mime =
Roar::extensionToMime(file.extension().string()).value_or("application/octet-stream");
// Return file
return CustomSchemeResponse{
.statusCode = content.empty() ? 204 : 200,
.reasonPhrase = "OK",
.headers =
{
{"Content-Type"s, mime},
// Do not forget to allow CORS
{"Access-Control-Allow-Origin"s, "*"s},
},
// Currently there is no streaming way to write the body, if its large.
.body = content,
};
},
// Windows: Is this secure like https (not http)? A lot of things are not allowed in http.
.treatAsSecure = true,
// Windows: Do urls to this custom scheme have an authority component? (For portability reasons, they usually should have).
.hasAuthorityComponent = true,
};
Window window{
WindowOptions{
// Not needed! Do not disable security on windows!
//.browserArguments = "--disable-web-security"s,
.title = "Test Project"s,
.debug = true,
.customSchemes = {scheme},
},
};
window.setSize(800, 600, Nui::WebViewHint::WEBVIEW_HINT_NONE);
window.centerOnPrimaryDisplay();
window.setHtml(index());
window.run();
}
On windows, use app.example
as host name for custom scheme urls, because its excluded from the DNS resolver, avoiding a 2 second delay.
In the frontend you can then use the custom scheme:
.foo {
background: url(nui://app.example/background.png);
}
import EditorWorker from 'url:nui://app.example/monaco-editor/esm/vs/editor/editor.worker.js';
Virtual Host Name to Folder Mapping
Currently 2 different ways of file mappings enabled via Window::setVirtualHostNameToFolderMapping
are needed.
setVirtualHostNameToFolderMapping maps a host name under the assets:// scheme (https: on windows) to a folder.
.remove-button-win{
background-image: url("https://assets/red_cross.svg");
}
.remove-button-nix {
background-image: url("assets://assets/red_cross.svg");
}
Typescript/Javascript Code
Import using the url prefix to avoid inlining:
import EditorWorker from 'url:monaco-editor/esm/vs/editor/editor.worker.js';