@glassly/create-miniapp 0.1.0-dev.1
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/README.md +34 -0
- package/bin/create-miniapp +3 -0
- package/bin/index.ts +150 -0
- package/package.json +42 -0
- package/template/AGENTS.md +18 -0
- package/template/README.md +37 -0
- package/template/_gitignore +7 -0
- package/template/build.ts +81 -0
- package/template/icon.png +0 -0
- package/template/miniapp.json +19 -0
- package/template/package.json +22 -0
- package/template/src/background/index.ts +48 -0
- package/template/src/shared/channels.ts +26 -0
- package/template/src/ui/App.tsx +26 -0
- package/template/src/ui/index.html +13 -0
- package/template/src/ui/main.tsx +30 -0
- package/template/src/ui/styles.css +43 -0
- package/template/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @glassly/create-miniapp
|
|
2
|
+
|
|
3
|
+
Scaffold a new [Glassly](https://github.com/tetramo-labs/glassly)
|
|
4
|
+
miniapp project — the `bun create` starter for apps that run on Glassly
|
|
5
|
+
smart glasses.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bunx @glassly/create-miniapp my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
bun install
|
|
13
|
+
bun run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> **Bun-only.** The scaffolder and the generated project's tooling run under
|
|
17
|
+
> [Bun](https://bun.sh) — use `bun` / `bunx`, not `npx`.
|
|
18
|
+
|
|
19
|
+
The generated project comes wired with
|
|
20
|
+
[`@glassly/miniapp`](https://www.npmjs.com/package/@glassly/miniapp) (the SDK
|
|
21
|
+
runtime) and [`@glassly/miniapp-cli`](https://www.npmjs.com/package/@glassly/miniapp-cli)
|
|
22
|
+
(the `dev` / `build` / `pack` author flow), a `miniapp.json` manifest, and a
|
|
23
|
+
TypeScript setup. Dependency pins are stamped at publish time to the exact
|
|
24
|
+
SDK versions this scaffolder shipped with, so a generated project installs
|
|
25
|
+
on any release channel.
|
|
26
|
+
|
|
27
|
+
To publish a finished miniapp to the Glassly Miniapp Store, see
|
|
28
|
+
[`@glassly/cli`](https://www.npmjs.com/package/@glassly/cli) (`glassly login` /
|
|
29
|
+
`glassly publish`).
|
|
30
|
+
|
|
31
|
+
## Part of Glassly
|
|
32
|
+
|
|
33
|
+
Source lives in the [Glassly monorepo](https://github.com/tetramo-labs/glassly)
|
|
34
|
+
under `sdk/create-miniapp`. Issues and contributions welcome there.
|
package/bin/index.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs"
|
|
4
|
+
import * as path from "path"
|
|
5
|
+
import {intro, outro, text, select, isCancel, cancel, note} from "@clack/prompts"
|
|
6
|
+
|
|
7
|
+
type GlassesType = "display" | "camera"
|
|
8
|
+
|
|
9
|
+
interface CliFlags {
|
|
10
|
+
projectName?: string
|
|
11
|
+
type?: GlassesType
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv: string[]): CliFlags {
|
|
15
|
+
const flags: CliFlags = {}
|
|
16
|
+
for (const arg of argv) {
|
|
17
|
+
if (arg.startsWith("--type=")) {
|
|
18
|
+
const value = arg.slice("--type=".length)
|
|
19
|
+
if (value === "display" || value === "camera") {
|
|
20
|
+
flags.type = value
|
|
21
|
+
} else {
|
|
22
|
+
console.error(`Invalid --type value: "${value}". Expected "display" or "camera".`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
} else if (!arg.startsWith("-") && !flags.projectName) {
|
|
26
|
+
flags.projectName = arg
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return flags
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toKebabCase(str: string): string {
|
|
33
|
+
return str
|
|
34
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
35
|
+
.replace(/[\s_]+/g, "-")
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateProjectName(name: string): string | undefined {
|
|
40
|
+
if (!name.trim()) return "Project name is required"
|
|
41
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*$/.test(name)) {
|
|
42
|
+
return "Use letters, numbers, dashes, underscores, or spaces (cannot start with a symbol)"
|
|
43
|
+
}
|
|
44
|
+
if (fs.existsSync(path.resolve(process.cwd(), name))) {
|
|
45
|
+
return `Directory "${name}" already exists`
|
|
46
|
+
}
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bailIfCancelled<T>(value: T | symbol): T {
|
|
51
|
+
if (isCancel(value)) {
|
|
52
|
+
cancel("Cancelled.")
|
|
53
|
+
process.exit(0)
|
|
54
|
+
}
|
|
55
|
+
return value as T
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const flags = parseArgs(process.argv.slice(2))
|
|
59
|
+
|
|
60
|
+
intro("create-miniapp")
|
|
61
|
+
|
|
62
|
+
let projectName = flags.projectName
|
|
63
|
+
if (projectName) {
|
|
64
|
+
const err = validateProjectName(projectName)
|
|
65
|
+
if (err) {
|
|
66
|
+
cancel(err)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
projectName = bailIfCancelled(
|
|
71
|
+
await text({
|
|
72
|
+
message: "What's your project name?",
|
|
73
|
+
placeholder: "my-miniapp",
|
|
74
|
+
validate: validateProjectName,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let glassesType = flags.type
|
|
80
|
+
if (!glassesType) {
|
|
81
|
+
glassesType = bailIfCancelled(
|
|
82
|
+
await select<GlassesType>({
|
|
83
|
+
message: "What kind of glasses is this miniapp for?",
|
|
84
|
+
options: [
|
|
85
|
+
{
|
|
86
|
+
value: "display",
|
|
87
|
+
label: "Display glasses",
|
|
88
|
+
hint: "Even Realities G1/G2, Vuzix Z100",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
value: "camera",
|
|
92
|
+
label: "Camera glasses",
|
|
93
|
+
hint: "Mentra Live",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
101
|
+
const templateDir = path.resolve(import.meta.dirname, "..", "template")
|
|
102
|
+
|
|
103
|
+
const kebabName = toKebabCase(projectName)
|
|
104
|
+
const packageName = `com.glassly.${kebabName}`
|
|
105
|
+
const hardwareRequirements =
|
|
106
|
+
glassesType === "camera"
|
|
107
|
+
? [
|
|
108
|
+
{type: "CAMERA", level: "REQUIRED"},
|
|
109
|
+
{type: "MICROPHONE", level: "REQUIRED"},
|
|
110
|
+
]
|
|
111
|
+
: [
|
|
112
|
+
{type: "DISPLAY", level: "REQUIRED"},
|
|
113
|
+
{type: "MICROPHONE", level: "REQUIRED"},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
function renderFile(srcPath: string, destPath: string): void {
|
|
117
|
+
if (path.basename(srcPath) === "miniapp.json") {
|
|
118
|
+
const raw = fs.readFileSync(srcPath, "utf-8")
|
|
119
|
+
const manifest = JSON.parse(raw.replace(/\{\{packageName\}\}/g, packageName))
|
|
120
|
+
manifest.hardwareRequirements = hardwareRequirements
|
|
121
|
+
fs.writeFileSync(destPath, JSON.stringify(manifest, null, 2) + "\n")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = fs.readFileSync(srcPath, "utf-8").replace(/\{\{packageName\}\}/g, packageName)
|
|
126
|
+
fs.writeFileSync(destPath, content)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function copyDir(src: string, dest: string): void {
|
|
130
|
+
fs.mkdirSync(dest, {recursive: true})
|
|
131
|
+
for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
|
|
132
|
+
const srcPath = path.join(src, entry.name)
|
|
133
|
+
// npm strips literal `.gitignore` files on publish; the template
|
|
134
|
+
// ships it as `_gitignore` instead and we rename on copy so the
|
|
135
|
+
// scaffolded project starts with a proper `.gitignore`.
|
|
136
|
+
const renamed = entry.name === "_gitignore" ? ".gitignore" : entry.name
|
|
137
|
+
const destPath = path.join(dest, renamed)
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
copyDir(srcPath, destPath)
|
|
140
|
+
} else {
|
|
141
|
+
renderFile(srcPath, destPath)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
copyDir(templateDir, targetDir)
|
|
147
|
+
|
|
148
|
+
note(`cd ${projectName}\nbun install\nbun dev`, "Next steps")
|
|
149
|
+
|
|
150
|
+
outro(`Created miniapp in ./${projectName}`)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glassly/create-miniapp",
|
|
3
|
+
"version": "0.1.0-dev.1",
|
|
4
|
+
"description": "Scaffold a new Glassly miniapp project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"tag": "dev"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"create-miniapp": "bin/create-miniapp"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "echo 'No build needed \u2014 runs via bun'"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@clack/prompts": "^1.2.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.0.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"template"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"glassly",
|
|
28
|
+
"glassly",
|
|
29
|
+
"miniapp",
|
|
30
|
+
"create",
|
|
31
|
+
"scaffold"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/tetramo-labs/glassly.git",
|
|
36
|
+
"directory": "sdk/create-miniapp"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/tetramo-labs/glassly/tree/dev/sdk/create-miniapp#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/tetramo-labs/glassly/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Miniapp implementation rules
|
|
2
|
+
|
|
3
|
+
- Put glasses subscriptions, `session.*` calls, durable state, and logic that
|
|
4
|
+
must survive UI closure in `src/background/`.
|
|
5
|
+
- Put React, rendering, DOM access, and browser UI libraries in `src/ui/`.
|
|
6
|
+
- The layers do not share memory. Exchange serializable data through the typed
|
|
7
|
+
`glassly.*` / `session.ui.*` channels declared in `src/shared/channels.ts`.
|
|
8
|
+
- The background is a bare JavaScript engine, not a browser or Node. Supported
|
|
9
|
+
globals are `console`, timers, `queueMicrotask`, `fetch`, `WebSocket`,
|
|
10
|
+
`localStorage`, `crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`,
|
|
11
|
+
`TextDecoder`, `atob`, `btoa`, and the SDK-supported `AbortController` subset.
|
|
12
|
+
- Do not use `window`, `document`, DOM elements, `performance`,
|
|
13
|
+
`XMLHttpRequest`, `crypto.subtle`, Node built-ins, `process`, or runtime module
|
|
14
|
+
resolution in `src/background/`. Use `Date.now()` instead of
|
|
15
|
+
`performance.now()`.
|
|
16
|
+
- `bun run dev` is live and temporary. Multiple dev miniapps can coexist when
|
|
17
|
+
they use distinct manifest package names. Use `bun run release` for a
|
|
18
|
+
persistent local install that runs without the development computer.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Glassly miniapp
|
|
2
|
+
|
|
3
|
+
This project has two runtimes with different jobs:
|
|
4
|
+
|
|
5
|
+
- `src/background/` is always on while the miniapp is enabled. Put glasses
|
|
6
|
+
subscriptions, `session.*` calls, durable state, and logic that must keep
|
|
7
|
+
running after the UI closes here.
|
|
8
|
+
- `src/ui/` is a normal WebView that exists only while the user has the miniapp
|
|
9
|
+
open. Put React, rendering, DOM access, and browser UI libraries here.
|
|
10
|
+
|
|
11
|
+
The background is a bare JavaScript engine, not a browser or Node. It supports
|
|
12
|
+
`console`, timers, `fetch`, `WebSocket`, `localStorage`,
|
|
13
|
+
`crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`, `TextDecoder`,
|
|
14
|
+
`atob`, `btoa`, and the SDK-supported `AbortController` subset. It does not
|
|
15
|
+
support `window`, `document`, DOM elements, `performance`, `XMLHttpRequest`,
|
|
16
|
+
`crypto.subtle`, Node built-ins, `process`, or runtime module resolution. Use
|
|
17
|
+
`Date.now()` instead of `performance.now()`.
|
|
18
|
+
|
|
19
|
+
`bun run build` checks background source (including imported shared files) and
|
|
20
|
+
reports common unsupported browser or Node APIs with a file, line, and supported
|
|
21
|
+
alternative before it emits a bundle.
|
|
22
|
+
|
|
23
|
+
The layers do not share memory. Exchange serializable data through the typed
|
|
24
|
+
`glassly.*` / `session.ui.*` message bus declared in `src/shared/channels.ts`.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun run dev # live, temporary; keep the computer and CLI running
|
|
30
|
+
bun run release # persistent local install that runs without the computer
|
|
31
|
+
bun run build # build both layers into dist/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The Glassly App keeps one dev entry per manifest package name, so you can scan and
|
|
35
|
+
test multiple dev miniapps side by side. It also caches each entry's name and
|
|
36
|
+
icon. Rescanning the same package updates that entry. Use `bun run release` when
|
|
37
|
+
you want the miniapp to run without the computer.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production build script — two-output bundle.
|
|
3
|
+
*
|
|
4
|
+
* Two-layer miniapps ship TWO bundles in one zip:
|
|
5
|
+
* - dist/background/index.js — JSContext entry, no DOM
|
|
6
|
+
* - dist/ui/index.html + assets — WebView entry, full DOM
|
|
7
|
+
*
|
|
8
|
+
* Bun.build() handles both. The background bundle uses `format: "iife"`
|
|
9
|
+
* because JSC + Zipline/QuickJS evaluate it as a classic script — any
|
|
10
|
+
* top-level ESM `export` is a SyntaxError there. `@glassly/miniapp/background`
|
|
11
|
+
* is bundled in (not external) because the JSContext has no module
|
|
12
|
+
* resolver; the host's polyfill installs `__dispatch`/`__deliver` but
|
|
13
|
+
* does NOT provide the SDK surface. The UI bundle uses target: "browser"
|
|
14
|
+
* with no externals.
|
|
15
|
+
*
|
|
16
|
+
* Build-time env vars: any `PUBLIC_*` key in your shell or
|
|
17
|
+
* miniapp-local `.env` gets inlined into both bundles via define.
|
|
18
|
+
* Anything inlined into the UI bundle is visible in WebView network
|
|
19
|
+
* requests and source maps — secrets MUST live behind your own backend
|
|
20
|
+
* rather than in PUBLIC_*.
|
|
21
|
+
*
|
|
22
|
+
* Add bundler plugins (Tailwind, Vue, etc.) by pushing them into the
|
|
23
|
+
* matching `plugins` array. For example, with Tailwind v4 (UI side only):
|
|
24
|
+
*
|
|
25
|
+
* const tailwind = (await import("bun-plugin-tailwind")).default
|
|
26
|
+
* uiBuild.plugins.push(tailwind)
|
|
27
|
+
*
|
|
28
|
+
* Output goes to ./dist with the shape `glassly-miniapp release` expects.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {rm} from "fs/promises"
|
|
32
|
+
import {backgroundRuntimeGuardPlugin, reactSingletonPlugin} from "@glassly/miniapp-cli/build-helpers"
|
|
33
|
+
|
|
34
|
+
const distDir = "./dist"
|
|
35
|
+
|
|
36
|
+
// Wipe dist/ so old chunks don't accumulate across builds.
|
|
37
|
+
await rm(distDir, {recursive: true, force: true})
|
|
38
|
+
|
|
39
|
+
// Pick up PUBLIC_* env vars from the shell / .env. Bun loads .env
|
|
40
|
+
// from cwd automatically; we just need to forward the keys to define.
|
|
41
|
+
const define: Record<string, string> = {}
|
|
42
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
43
|
+
if (k.startsWith("PUBLIC_") && typeof v === "string") {
|
|
44
|
+
define[`process.env.${k}`] = JSON.stringify(v)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const backgroundResult = await Bun.build({
|
|
49
|
+
entrypoints: ["./src/background/index.ts"],
|
|
50
|
+
outdir: `${distDir}/background`,
|
|
51
|
+
target: "browser",
|
|
52
|
+
format: "iife",
|
|
53
|
+
plugins: [backgroundRuntimeGuardPlugin(import.meta.url)],
|
|
54
|
+
minify: false,
|
|
55
|
+
define,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!backgroundResult.success) {
|
|
59
|
+
console.error("Background build failed:")
|
|
60
|
+
for (const log of backgroundResult.logs) console.error(log)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const uiResult = await Bun.build({
|
|
65
|
+
entrypoints: ["./src/ui/index.html"],
|
|
66
|
+
outdir: `${distDir}/ui`,
|
|
67
|
+
target: "browser",
|
|
68
|
+
plugins: [reactSingletonPlugin(import.meta.url)],
|
|
69
|
+
minify: true,
|
|
70
|
+
define,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!uiResult.success) {
|
|
74
|
+
console.error("UI build failed:")
|
|
75
|
+
for (const log of uiResult.logs) console.error(log)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Silence on success — the dev-server's `reload →` line is the
|
|
80
|
+
// developer-facing confirmation. Failures already print via the
|
|
81
|
+
// .success branches above.
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@glassly/miniapp-cli/schema/miniapp.schema.json",
|
|
3
|
+
"packageName": "{{packageName}}",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"name": "My Miniapp",
|
|
6
|
+
"description": "A Glassly miniapp",
|
|
7
|
+
"icon": "icon.png",
|
|
8
|
+
"type": "standard",
|
|
9
|
+
"sdkVersion": "0.3.0",
|
|
10
|
+
"minHostVersion": "1.42.0",
|
|
11
|
+
"entry": {
|
|
12
|
+
"background": "background/index.js",
|
|
13
|
+
"ui": "ui/index.html"
|
|
14
|
+
},
|
|
15
|
+
"permissions": [],
|
|
16
|
+
"hardwareRequirements": [
|
|
17
|
+
{"type": "DISPLAY", "level": "REQUIRED", "description": "Renders text + UI on glasses"}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "glassly-miniapp dev",
|
|
6
|
+
"build": "bun run build.ts",
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"release": "glassly-miniapp release",
|
|
9
|
+
"pack": "glassly-miniapp pack"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@glassly/miniapp": "0.1.0-dev.62",
|
|
13
|
+
"react": "^19.0.0",
|
|
14
|
+
"react-dom": "^19.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@glassly/miniapp-cli": "0.1.0-dev.1",
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"typescript": "^5.6.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background JSContext entry point. The Glassly host loads this file
|
|
3
|
+
* inside a per-miniapp JSContext (iOS-JSC / Android-QuickJS) and calls
|
|
4
|
+
* the `registerMiniapp` handler once CONNECT completes. All glasses-side
|
|
5
|
+
* logic lives in here.
|
|
6
|
+
*
|
|
7
|
+
* Background runs **always** while the host app process is alive — even
|
|
8
|
+
* when the user hasn't opened the miniapp's UI. Register your event
|
|
9
|
+
* subscriptions inside the handler; the runtime tears them down when the
|
|
10
|
+
* miniapp is disabled or uninstalled.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {registerMiniapp} from "@glassly/miniapp/background"
|
|
14
|
+
import type {Channels} from "../shared/channels"
|
|
15
|
+
|
|
16
|
+
registerMiniapp<Channels>(async (session) => {
|
|
17
|
+
// Hydrate any persisted state from session.storage (string KV — JSON-encode
|
|
18
|
+
// structured data yourself).
|
|
19
|
+
// const stored = await session.storage.get("notes")
|
|
20
|
+
// const notes: Note[] = stored ? JSON.parse(stored) : []
|
|
21
|
+
|
|
22
|
+
// Glasses button → demo display. display.render() replaces the whole frame:
|
|
23
|
+
// describe everything that should be on screen and the host diffs it against
|
|
24
|
+
// the previous frame (stable ids update in place; render([]) clears). Box
|
|
25
|
+
// coordinates are raw device px — read `session.capabilities.display`
|
|
26
|
+
// (populated on the "ready" event) for the real canvas size.
|
|
27
|
+
session.input.onButtonPress(() => {
|
|
28
|
+
const d = session.capabilities?.display
|
|
29
|
+
void session.display.render([
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
id: "hello",
|
|
33
|
+
box: {x: 0, y: 0, w: d?.width ?? 576, h: d?.height ?? 288},
|
|
34
|
+
text: "Hello from GlasslyJS",
|
|
35
|
+
},
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// UI bus: forward "ping" → "pong" with a roundtrip timestamp.
|
|
40
|
+
session.ui.onOpen(() => {
|
|
41
|
+
// Push initial state to the WebView whenever it opens.
|
|
42
|
+
session.ui.send("pong", {at: Date.now(), roundtripMs: 0})
|
|
43
|
+
})
|
|
44
|
+
session.ui.on("ping", ({at}) => {
|
|
45
|
+
const roundtripMs = Date.now() - at
|
|
46
|
+
session.ui.send("pong", {at: Date.now(), roundtripMs})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed channel registry — single source of truth for the names + payload
|
|
3
|
+
* shapes that flow between this miniapp's background JSContext and its UI
|
|
4
|
+
* WebView. Both halves import this file at build time; the bundler inlines
|
|
5
|
+
* the declarations so there's no runtime resolution.
|
|
6
|
+
*
|
|
7
|
+
* Add a key per channel. The TypeScript generic on `glassly.send` /
|
|
8
|
+
* `glassly.on` / `session.ui.send` / `session.ui.on` enforces names + payload
|
|
9
|
+
* shapes at compile time so the two halves can't drift.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface Channels {
|
|
13
|
+
// WebView → background
|
|
14
|
+
"ping": {at: number}
|
|
15
|
+
|
|
16
|
+
// background → WebView
|
|
17
|
+
"pong": {at: number; roundtripMs: number}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
// Augment the `glassly` global from @glassly/miniapp/ui with this miniapp's
|
|
22
|
+
// typed channel registry so authors get compile-time enforcement on every
|
|
23
|
+
// glassly.send / glassly.on call without re-declaring it.
|
|
24
|
+
// eslint-disable-next-line no-var
|
|
25
|
+
var glassly: import("@glassly/miniapp/ui").TypedUiGlobal<Channels>
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {useEffect, useState} from "react"
|
|
2
|
+
|
|
3
|
+
export function App() {
|
|
4
|
+
const [roundtripMs, setRoundtripMs] = useState<number | null>(null)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const unsub = glassly.on("pong", ({roundtripMs}) => {
|
|
8
|
+
setRoundtripMs(roundtripMs)
|
|
9
|
+
})
|
|
10
|
+
return unsub
|
|
11
|
+
}, [])
|
|
12
|
+
|
|
13
|
+
const ping = () => {
|
|
14
|
+
glassly.send("ping", {at: Date.now()})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="app">
|
|
19
|
+
<h1>{`{{packageName}}`}</h1>
|
|
20
|
+
<p>Round-trip to background: {roundtripMs == null ? "—" : `${roundtripMs} ms`}</p>
|
|
21
|
+
<button type="button" onClick={ping}>
|
|
22
|
+
Ping background
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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, user-scalable=no" />
|
|
6
|
+
<title>{{packageName}}</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="./main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebView entry point. Imports the `glassly` global from the auto-injected
|
|
3
|
+
* host shim and the typed `Channels` registry from src/shared/. The
|
|
4
|
+
* WebView has zero direct native access — all hardware calls (glasses
|
|
5
|
+
* display, sensors, BLE, etc.) go through `glassly.send(channel, payload)`
|
|
6
|
+
* to background, which translates them into `session.*` SDK calls.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {createRoot} from "react-dom/client"
|
|
10
|
+
import {MiniappProvider} from "@glassly/miniapp/ui"
|
|
11
|
+
import "../shared/channels"
|
|
12
|
+
import {App} from "./App"
|
|
13
|
+
import "./styles.css"
|
|
14
|
+
|
|
15
|
+
const root = document.getElementById("root")
|
|
16
|
+
if (!root) {
|
|
17
|
+
throw new Error("Missing #root element in index.html")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// MiniappProvider syncs <html class="dark"> with the host color scheme.
|
|
21
|
+
// Purely a CSS theme bridge — does NOT construct a MiniappSession.
|
|
22
|
+
createRoot(root).render(
|
|
23
|
+
<MiniappProvider>
|
|
24
|
+
<App />
|
|
25
|
+
</MiniappProvider>,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// MUST call glassly.ready() on bootstrap so the host knows the WebView is
|
|
29
|
+
// mounted and can flush any buffered `session.ui.send` calls.
|
|
30
|
+
glassly.ready()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
background: #0b0b0b;
|
|
9
|
+
color: #f5f5f5;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.app {
|
|
17
|
+
text-align: center;
|
|
18
|
+
padding: 24px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
h1 {
|
|
22
|
+
font-size: 1.5rem;
|
|
23
|
+
margin: 0 0 12px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
p {
|
|
27
|
+
margin: 8px 0 24px;
|
|
28
|
+
opacity: 0.7;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
button {
|
|
32
|
+
font-size: 1rem;
|
|
33
|
+
padding: 12px 24px;
|
|
34
|
+
border-radius: 12px;
|
|
35
|
+
border: none;
|
|
36
|
+
background: #4a9eff;
|
|
37
|
+
color: white;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button:active {
|
|
42
|
+
transform: scale(0.97);
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": ".",
|
|
12
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
15
|
+
}
|