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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/create-gesso-app.mjs +400 -0
- package/dist/create-gesso-app.mjs.map +1 -0
- package/package.json +47 -0
- package/templates/electrobun/README.md +209 -0
- package/templates/electrobun/_gitignore +10 -0
- package/templates/electrobun/electrobun.config.ts +49 -0
- package/templates/electrobun/hutch.config.ts +48 -0
- package/templates/electrobun/package.json +20 -0
- package/templates/electrobun/src/main/index.ts +90 -0
- package/templates/electrobun/src/render/App.tsx +100 -0
- package/templates/electrobun/src/shared/Counter.ts +28 -0
- package/templates/electrobun/src/shared/rpc.ts +22 -0
- package/templates/electrobun/src/view/index.html +31 -0
- package/templates/electrobun/src/view/main.ts +53 -0
- package/templates/electrobun/src/view/render.worker.ts +15 -0
- package/templates/electrobun/tsconfig.json +28 -0
- package/templates/electrobun/vite.config.ts +32 -0
- package/templates/web/README.md +88 -0
- package/templates/web/_gitignore +4 -0
- package/templates/web/index.html +28 -0
- package/templates/web/package.json +24 -0
- package/templates/web/src/App.tsx +63 -0
- package/templates/web/src/main.ts +31 -0
- package/templates/web/src/worker.ts +12 -0
- package/templates/web/tsconfig.json +20 -0
- package/templates/web/vite.config.ts +23 -0
|
@@ -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,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
|
+
});
|