create-gesso-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The render worker: components, layout, paint and hit testing.
3
+ *
4
+ * It attaches to its channels by name and never learns that the other
5
+ * end of them is in another process. The same file in a web
6
+ * application says the same thing; only what is behind the channel
7
+ * changes.
8
+ */
9
+ import { DesktopWindows } from 'gesso-electrobun/desktop';
10
+ import { renderRoot } from 'gesso-framework';
11
+
12
+ import { App } from '../render/App';
13
+ import { Counter } from '../shared/Counter';
14
+
15
+ renderRoot(App).useChannel(Counter).useChannel(DesktopWindows);
@@ -0,0 +1,28 @@
1
+ {
2
+ /*
3
+ Written by `hutch electrobun prepare`. It is what makes
4
+ `electrobun/main` and `electrobun/view` resolve, so a typecheck run
5
+ before the first prepare has nothing to resolve them against.
6
+ */
7
+ "extends": "./.hutch/devkit/tsconfig.json",
8
+ "compilerOptions": {
9
+ "target": "ESNext",
10
+ "module": "ESNext",
11
+ "lib": ["ES2023", "DOM", "DOM.Iterable", "WebWorker"],
12
+ "types": ["@types/bun"],
13
+
14
+ /* The Gesso packages ship `exports` maps, so this is the line that has to be right. */
15
+ "moduleResolution": "bundler",
16
+ "esModuleInterop": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "strict": true,
20
+ "noEmit": true,
21
+
22
+ /* The two lines that let you write elements as markup. */
23
+ "jsx": "react-jsx",
24
+ "jsxImportSource": "gesso-framework"
25
+ },
26
+ "include": ["src"],
27
+ "exclude": ["node_modules", "dist", "build"]
28
+ }
@@ -0,0 +1,32 @@
1
+ import { resolve } from 'node:path';
2
+ import { defineConfig } from 'vite';
3
+
4
+ import { electrobunViteAliases } from './.hutch/devkit/api/config/electrobun-vite';
5
+
6
+ /**
7
+ * The window's assets: an ordinary Vite build with one alias list.
8
+ *
9
+ * The aliases are for Electrobun and not for Gesso. `electrobun/view`
10
+ * is projected into `.hutch/devkit` by `hutch electrobun prepare`
11
+ * rather than installed, so Vite has to be told where it went; the
12
+ * Gesso packages are ordinary dependencies and need nothing. This file
13
+ * therefore cannot be loaded before the first prepare, which is why
14
+ * every script in `hutch.config.ts` runs prepare first.
15
+ *
16
+ * `root` is the view, so `src/main` is never built here: the main
17
+ * process is Electrobun's to bundle, out of `electrobun.config.ts`.
18
+ * The render worker needs no configuration at all, because
19
+ * `new Worker(new URL(...))` is an expression Vite already splits into
20
+ * its own chunk.
21
+ */
22
+ export default defineConfig({
23
+ resolve: {
24
+ // Spread rather than passed through, so an alias of your own has
25
+ // somewhere obvious to go.
26
+ alias: [...electrobunViteAliases(resolve(__dirname, '.hutch/devkit'))]
27
+ },
28
+ esbuild: { jsx: 'automatic', jsxImportSource: 'gesso-framework' },
29
+ root: 'src/view',
30
+ build: { outDir: '../../dist', emptyOutDir: true },
31
+ server: { port: 5173, strictPort: true }
32
+ });
@@ -0,0 +1,88 @@
1
+ # {{name}}
2
+
3
+ A Gesso application. The interface is built, laid out, painted and
4
+ hit-tested in a render worker; the page's own thread creates the canvas,
5
+ forwards input and does nothing else.
6
+
7
+ ```bash
8
+ pnpm install # or npm install
9
+ pnpm dev
10
+ ```
11
+
12
+ Then `pnpm build` for a production bundle, `pnpm preview` to serve it,
13
+ and `pnpm typecheck` to check the types without building.
14
+
15
+ ## The three files
16
+
17
+ | File | What it is |
18
+ | --------------- | -------------------------------------------------------- |
19
+ | `src/main.ts` | The main thread: create the app and mount it into `#app` |
20
+ | `src/worker.ts` | The render worker: name the root component |
21
+ | `src/App.tsx` | The screen |
22
+
23
+ There are three rather than two because a component cannot cross
24
+ `postMessage`, so the root has to be named on the worker's side of the
25
+ barrier.
26
+
27
+ `main.ts` names no worker, and that is `gesso-vite-plugin` in
28
+ `vite.config.ts`. It finds `worker.ts` beside `main.ts` and writes the
29
+ construction, which has to be written out as a literal because a bundler
30
+ emits a chunk for a worker it can see constructed and cannot see through
31
+ a variable holding the URL. The plugin also, while the dev server is
32
+ running:
33
+
34
+ - replaces the screen when you save `App.tsx`, instead of reloading the
35
+ page, and puts the keyboard focus back where it was;
36
+ - draws what the render worker threw over the application that was
37
+ running when it threw it, source-mapped, with the node named as a path
38
+ through your components rather than as an id;
39
+ - says so when a save is about to reload the page, which happens when a
40
+ module is reachable from the main thread as well as from the worker.
41
+
42
+ If you would rather say it out loud, write it and the plugin leaves the
43
+ construction alone:
44
+
45
+ ```ts
46
+ createApp({
47
+ renderWorker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
48
+ });
49
+ ```
50
+
51
+ ```json
52
+ "jsx": "react-jsx",
53
+ "jsxImportSource": "gesso-framework"
54
+ ```
55
+
56
+ Those two lines in `tsconfig.json` are the whole of what makes `<row>`
57
+ and `<text>` work. JSX here is a spelling rather than a runtime: it
58
+ compiles onto the element factories and produces the identical tree, so
59
+ `Row({ gap: 8 }, Text({ text: 'Ready' }))` is the same thing written the
60
+ other way.
61
+
62
+ ## Reporting errors from a production build
63
+
64
+ The overlay is a development tool and a production build carries no
65
+ reference to it. What a shipped application wants instead is its own
66
+ `onError`, which takes the same three arguments:
67
+
68
+ ```ts
69
+ createApp({
70
+ onError: (message, stack, source) => {
71
+ reportToYourService({ message, stack, source });
72
+ }
73
+ });
74
+ ```
75
+
76
+ `source` is one of `message`, `uncaught`, `renderer`, `listener` or
77
+ `channel`, and it says what the failure cost: a `listener` error means
78
+ one handler did not run, a `renderer` error means the surface stopped
79
+ being updated, and a `channel` error means the data behind an intact
80
+ view has stopped arriving.
81
+
82
+ ## Where to go next
83
+
84
+ - `App.tsx` is commented with what each part of it is doing.
85
+ - `gesso-components` has the controls: inputs, overlays, structure,
86
+ data and media. `Switch` in `App.tsx` is one of them.
87
+ - Every prop takes a value or an Observable of that value. That is the
88
+ whole binding model, and it is why the component body runs once.
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.local
4
+ .DS_Store
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ <style>
8
+ /*
9
+ The canvas is sized to its host, so the host needs a size. This
10
+ is the whole of the CSS a Gesso application needs; everything
11
+ else on the page is painted, not styled.
12
+ */
13
+ html,
14
+ body {
15
+ margin: 0;
16
+ height: 100%;
17
+ }
18
+ #app {
19
+ width: 100vw;
20
+ height: 100vh;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div id="app"></div>
26
+ <script type="module" src="/src/main.ts"></script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "gesso-app",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "gesso-components": "^0.1.0",
14
+ "gesso-core": "^0.1.0",
15
+ "gesso-framework": "^0.1.0",
16
+ "rxjs": "^7.8.2"
17
+ },
18
+ "devDependencies": {
19
+ "gesso-devtools": "^0.1.0",
20
+ "gesso-vite-plugin": "^0.1.0",
21
+ "typescript": "~7.0.2",
22
+ "vite": "^8.2.0"
23
+ }
24
+ }
@@ -0,0 +1,63 @@
1
+ import { map } from 'rxjs/operators';
2
+
3
+ import { percent } from 'gesso-core';
4
+ import { Switch } from 'gesso-components';
5
+ import { internalState, type ComponentContext, type Inputs } from 'gesso-framework';
6
+
7
+ /**
8
+ * The screen.
9
+ *
10
+ * A Gesso component is a function that runs once. What it returns is a
11
+ * tree of nodes that stays: nothing here re-runs when the count
12
+ * changes, because `text` is bound to an Observable, so one property on
13
+ * one node is written and the next frame is drawn from it.
14
+ *
15
+ * Three things are worth knowing before you change them.
16
+ *
17
+ * - `internalState(0)` is state this component owns. Writing
18
+ * `count.value++` marks exactly the bindings that read it, and
19
+ * nothing else.
20
+ * - A bound property costs what it touches. `text` is content and
21
+ * `opacity` is paint, so neither re-runs layout; a bound `width` or
22
+ * `gap` would.
23
+ * - No colour here is a hex value. `primary`, `background` and
24
+ * `textMuted` are names looked up on whichever theme the node
25
+ * inherits, so this screen follows a theme it never mentions.
26
+ *
27
+ * The elements are lowercase because they are intrinsic, resolved by
28
+ * `jsxImportSource` in `tsconfig.json`, the same way `<div>` needs no
29
+ * import in React. `Switch` is capitalised because it is a component
30
+ * from `gesso-components`, so it is imported like any other value.
31
+ */
32
+ export function App(_inputs: Inputs<{}>, _context: ComponentContext) {
33
+ const count = internalState(0);
34
+ const hinted = internalState(true);
35
+
36
+ return (
37
+ <column gap={16} x="center" y="center" width={percent(100)} height={percent(100)} backgroundColor="background">
38
+ <text text="Hello from a render worker" fontSize={22} fontWeight="bold" />
39
+
40
+ <row gap={12} y="center">
41
+ <text text={count.pipe(map(value => `Clicks: ${value}`))} fontSize={16} />
42
+ <button
43
+ label="Add one"
44
+ onClick={() => count.value++}
45
+ padding={8}
46
+ borderRadius={6}
47
+ backgroundColor="primary"
48
+ cursor="pointer">
49
+ <text text="+1" color="background" fontSize={14} />
50
+ </button>
51
+ </row>
52
+
53
+ <text
54
+ text={count.pipe(map(value => (value === 0 ? 'Press the button.' : 'Nothing was rebuilt to do that.')))}
55
+ fontSize={12}
56
+ color="textMuted"
57
+ opacity={hinted.pipe(map(showing => (showing ? 1 : 0)))}
58
+ />
59
+
60
+ <Switch label="Show the hint" checked={hinted} onChange={next => (hinted.value = next)} />
61
+ </column>
62
+ );
63
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The main thread's entire job.
3
+ *
4
+ * It finds a host element, creates the app and mounts. Everything a
5
+ * person sees is built, laid out, painted and hit-tested in the worker;
6
+ * the page forwards input events and does nothing else, so work on this
7
+ * thread cannot delay a frame.
8
+ *
9
+ * The worker is not named here. `gesso-vite-plugin`, in
10
+ * `vite.config.ts`, finds `worker.ts` beside this file and writes the
11
+ * `new Worker(new URL(...))` construction, which is the only form a
12
+ * bundler emits a chunk for. It also draws the error overlay over the
13
+ * app when the worker throws, and gives the worker its hot-replacement
14
+ * wiring, so saving `App.tsx` redraws the screen without reloading the
15
+ * page. If you would rather say it out loud, write it and the plugin
16
+ * leaves it alone:
17
+ *
18
+ * createApp({
19
+ * renderWorker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
20
+ * });
21
+ */
22
+ import { createApp } from 'gesso-framework';
23
+
24
+ const host = document.querySelector<HTMLElement>('#app');
25
+ if (host === null) {
26
+ throw new Error('index.html has no #app element to mount into.');
27
+ }
28
+
29
+ const app = createApp();
30
+
31
+ app.mount(host);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The render worker: everything the person sees.
3
+ *
4
+ * A component cannot cross `postMessage`, so the root is named here
5
+ * rather than passed in from `main.ts`. That one constraint is the
6
+ * only reason this file exists, and it is why every Gesso application
7
+ * has three files rather than two.
8
+ */
9
+ import { renderRoot } from 'gesso-framework';
10
+ import { App } from './App';
11
+
12
+ renderRoot(App);
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2023",
4
+ "module": "esnext",
5
+ "lib": ["ES2023", "DOM"],
6
+ "types": ["vite/client"],
7
+
8
+ /* The packages ship `exports` maps, so this is the line that has to be right. */
9
+ "moduleResolution": "bundler",
10
+ "verbatimModuleSyntax": true,
11
+ "moduleDetection": "force",
12
+ "strict": true,
13
+ "noEmit": true,
14
+
15
+ /* The two lines that let you write elements as markup. */
16
+ "jsx": "react-jsx",
17
+ "jsxImportSource": "gesso-framework"
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,23 @@
1
+ import { gesso } from 'gesso-vite-plugin';
2
+ import { defineConfig } from 'vite';
3
+
4
+ /**
5
+ * An ordinary Vite project with one Gesso plugin.
6
+ *
7
+ * The plugin writes the three things every Gesso application would
8
+ * otherwise write the same way by hand: the `new Worker(new URL(...))`
9
+ * construction for the render worker, the `import.meta.hot.accept`
10
+ * wiring that replaces the screen on a save instead of reloading the
11
+ * page, and the error overlay that draws what the worker threw over
12
+ * the application that was running when it threw it. The last two are
13
+ * development only; a production build carries no reference to
14
+ * `gesso-devtools` at all.
15
+ *
16
+ * None of it is required. Write `renderWorker` in `createApp` yourself
17
+ * and the plugin leaves the construction alone; take the plugin out
18
+ * altogether and the application still runs, with the incantations
19
+ * back in `main.ts`.
20
+ */
21
+ export default defineConfig({
22
+ plugins: [gesso()]
23
+ });