Your first UI
Build a web page from one public module declared by a module.json, with its own stylesheet, a declared SVG asset and source maps; serve it with the Beyond command line, see it in a browser, change its CSS without reloading, and request its production output.
- Availability: Experimental
- Evidence: Recorded run
- Tutorial
What you build
One page that says Hello Beyond! in a styled heading with a small logo. It is one public module, @testbed/web-hello/main, declared by a module.json and compiled in the esbuild packaging mode. No widget, no view framework, no custom compiler: a manifest, a TypeScript entry point, a stylesheet and a declared asset.
Declare the package and the module
The package selects the esbuild bundler for every module and names the compiler it runs, the esbuild installed with the toolchain:
{
"name": "@testbed/web-hello",
"version": "0.1.0",
"private": true,
"description": "A manifest-first web module with its own stylesheet and a declared asset, compiled in the esbuild packaging mode",
"beyond": {
"modules": ".",
"bundler": "esbuild"
},
"bundlers": {
"esbuild": {
"specifier": "@beyond-js/packages/bundlers/esbuild",
"processors": {
"bundle": {
"compiler": "esbuild"
}
}
}
}
}The module is declared by its manifest: where it runs, its entry point and the static files it may serve. Nothing else declares it; exports is not needed.
{
"platforms": ["web"],
"entry": "index.ts",
"assets": ["logo.svg"]
}Write the entry point, the stylesheet and the asset
The entry point imports the stylesheet and the logo, and puts the greeting on the page. A preview imports the module and calls nothing, so this top-level code is what mounts the page:
/**
* The entry point of `@testbed/web-hello/main`. Importing it puts the greeting on the page: the preview
* document imports the entry module and calls nothing. The stylesheet the module imports is delivered as a
* separate resource beside the code, and the logo is a declared static file addressed from the module.
*/
import './styles.css';
import logo from './logo.svg';
import { greeting } from './greeting';
const article = document.createElement('article');
article.className = 'hello';
const title = document.createElement('h1');
title.className = 'greeting';
title.textContent = greeting('Beyond');
const image = document.createElement('img');
image.className = 'logo';
image.alt = 'Beyond';
image.src = logo;
article.append(title, image);
document.body.append(article);greeting.ts is an internal file: it is bundled into the artifact and is not a public module.
/**
* An internal file of the module: it is bundled into the artifact and is not a public module
*/
export const greeting = (subject: string): string => `Hello ${subject}!`;The stylesheet is an ordinary CSS file. It is not inlined into the code: it is delivered as a separate resource beside the module, and the page links it.
/* The stylesheet of the module: delivered beside its code, linked by the page, replaced without a reload */
.hello {
font-family: system-ui, sans-serif;
padding: 16px;
}
.greeting {
color: rgb(12, 74, 110);
font-size: 32px;
}
.logo {
width: 48px;
height: 48px;
display: block;
}logo.svg is any SVG. Because the manifest declares it under assets, it is served next to the module, and import logo from './logo.svg' gives its address, relative to the module.
Serve it and see it
Start the development server from the workspace directory and open the preview:
# From the workspace directory (the one with beyond.json). The toolchain that provides `beyond` is not in a
# public registry: use the installation your Beyond Workspace environment or your team provides.
BEYOND_SERVICE_EXTENSIONS=@beyond-js/packages/development beyond run
# <endpoint> is the address the development server printed when it started. Open <endpoint>/preview/ in a browser.
# The compiled module, with an inline source map that names index.ts and greeting.ts
curl -s "<endpoint>/m/@testbed/[email protected]/modules/main?target=browser&format=esm&env=development&min=false&sourcemap=inline&types=false&css=false"
# Its stylesheet, delivered beside the code and linked by the page
curl -s "<endpoint>/m/@testbed/[email protected]/styles/main?target=browser&format=esm&env=development&min=false&sourcemap=inline&types=false&css=false"
# The declared asset; a file the module does not declare answers 404 OUTPUT_NOT_AVAILABLE
curl -s "<endpoint>/m/@testbed/[email protected]/assets/main/logo.svg"
# The production output: minified, without the development runtime. Production is minified only.
curl -s "<endpoint>/m/@testbed/[email protected]/modules/main?target=browser&format=esm&env=production&min=true&sourcemap=none&types=false&css=false"The preview shows the heading in dark blue (rgb(12, 74, 110)) with the logo below it. The document links the stylesheet of the module in its head, marked data-beyond-styles="@testbed/[email protected]/main", and registers the development runtime, so what you edit reaches the page.
What each address delivers:
| Resource | What you get |
|---|---|
| The module | The compiled code with an inline source map that names index.ts and greeting.ts, and the asset addressed relatively (../assets/main/logo.svg) |
| The stylesheet | The CSS with its own inline map, as text/css |
| The asset | The SVG, as image/svg+xml |
A file the module does not declare, such as assets/main/index.ts |
404 with the code OUTPUT_NOT_AVAILABLE: nothing undeclared is ever served |
Change the CSS without reloading
Edit the color of .greeting in styles.css and save. The heading changes color in the open page: the service announces the new stylesheet, the runtime replaces the linked sheet, and the page does not navigate. A value you set in the page (window.sentinel = 42 in the acceptance) is still there, and the heading is the same DOM node.
Now add @import "./missing.css"; at the top of the file. The build fails: the module is invalid, the stylesheet answers 422 BUILD_FAILED with a BUNDLE_ERROR that names missing.css, and the page keeps the last good style. Remove the line, and the style comes back.
Use the production output
The same service answers the production conditional of the module: minified, without any development runtime and without source maps. It is what a page that was not built with Beyond loads from the origin that delivers the module:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="icon" href="data:,">
<!-- The production stylesheet and module of the public module, from the origin that delivers it -->
<link rel="stylesheet" href="<origin>/m/@testbed/[email protected]/styles/main?target=browser&format=esm&env=production&min=true&sourcemap=none&types=false&css=false">
<script type="module" src="<origin>/m/@testbed/[email protected]/modules/main?target=browser&format=esm&env=production&min=true&sourcemap=none&types=false&css=false"></script>
</head>
<body></body>
</html>The acceptance served this document from a second origin and loaded it in the browser: the greeting rendered with its computed style and the decoded logo, and nothing of the development service (/events, /u/) was requested. Production is minified only: asking for env=production&min=false is refused with OPTION_UNSUPPORTED.
Next
Add SCSS, Tailwind and a shared stylesheet: Styles. Build a widget whose view is a React, Vue or Svelte component: Author a Beyond widget.