@glassly/create-miniapp 0.1.0-beta.7
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 +66 -0
- package/bin/create-miniapp +20 -0
- package/bin/index.ts +190 -0
- package/package.json +42 -0
- package/template/AGENTS.md +18 -0
- package/template/README.md +43 -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 +23 -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,66 @@
|
|
|
1
|
+
# @glassly/create-miniapp
|
|
2
|
+
|
|
3
|
+
Scaffold a new [Glassly](https://github.com/tetramo-labs/glassly)
|
|
4
|
+
miniapp project. This is 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
|
+
> The scaffolder and the generated project's tooling run under
|
|
17
|
+
> [Bun](https://bun.sh) only. Use `bun` and `bunx`, not `npx`.
|
|
18
|
+
|
|
19
|
+
### `--prefix`
|
|
20
|
+
|
|
21
|
+
Every miniapp lives under its developer org's reverse-DNS package prefix (set
|
|
22
|
+
when you create the org in the Developer Console, for example `com.example`).
|
|
23
|
+
The scaffolder asks for it and writes `<prefix>.<project_name>` into
|
|
24
|
+
`miniapp.json` as the `packageName`, with dashes in the project name turned into
|
|
25
|
+
underscores so the name is valid. The Store rejects releases outside your org's
|
|
26
|
+
prefix, so use the real one. Pass `--prefix` to skip the prompt:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
bunx @glassly/create-miniapp my-app --prefix=com.example
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `--type`
|
|
33
|
+
|
|
34
|
+
The scaffolder asks what kind of glasses the miniapp targets, which decides the
|
|
35
|
+
`hardwareRequirements` it writes into `miniapp.json`. Pass `--type` to skip the
|
|
36
|
+
prompt:
|
|
37
|
+
|
|
38
|
+
| Value | Glasses | `hardwareRequirements` |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `display` | Even Realities G1/G2, Vuzix Z100 | `DISPLAY` + `MICROPHONE`, both `REQUIRED` |
|
|
41
|
+
| `camera` | Mentra Live | `CAMERA` + `MICROPHONE`, both `REQUIRED` |
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
bunx @glassly/create-miniapp my-app --type=display
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use the `--flag=value` form for both flags; a space-separated `--type value` is
|
|
48
|
+
not parsed.
|
|
49
|
+
Requirements are only a starting point — edit `miniapp.json` directly, or with
|
|
50
|
+
`glassly hardware add`, as the miniapp grows.
|
|
51
|
+
|
|
52
|
+
The generated project comes wired with
|
|
53
|
+
[`@glassly/miniapp`](https://www.npmjs.com/package/@glassly/miniapp) (the SDK
|
|
54
|
+
runtime) and [`@glassly/cli`](https://www.npmjs.com/package/@glassly/cli) (the
|
|
55
|
+
`glassly` command: `dev`, `release`, `pack`, `publish`), a `miniapp.json`
|
|
56
|
+
manifest, and a TypeScript setup. Dependency pins are stamped at publish time to the exact
|
|
57
|
+
SDK versions this scaffolder shipped with, so a generated project installs
|
|
58
|
+
on any release channel.
|
|
59
|
+
|
|
60
|
+
To publish a finished miniapp to the Glassly Miniapp Store, run `glassly login`
|
|
61
|
+
once and then `glassly publish` from the project folder.
|
|
62
|
+
|
|
63
|
+
## Part of Glassly
|
|
64
|
+
|
|
65
|
+
Source lives in the [Glassly monorepo](https://github.com/tetramo-labs/glassly)
|
|
66
|
+
under `sdk/create-miniapp`. Issues and contributions welcome there.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Extensionless launcher: npm strips bin entries that point at .ts files.
|
|
3
|
+
// The scaffolder runs under Bun; when invoked from Node (npx, npm create)
|
|
4
|
+
// delegate to Bun if it is installed, otherwise say how to get it.
|
|
5
|
+
import {spawnSync} from "node:child_process"
|
|
6
|
+
import {fileURLToPath} from "node:url"
|
|
7
|
+
|
|
8
|
+
const entry = fileURLToPath(new URL("./index.ts", import.meta.url))
|
|
9
|
+
|
|
10
|
+
if (process.versions.bun) {
|
|
11
|
+
await import(entry)
|
|
12
|
+
} else {
|
|
13
|
+
const result = spawnSync("bun", [entry, ...process.argv.slice(2)], {stdio: "inherit"})
|
|
14
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
15
|
+
console.error("create-miniapp runs on Bun, which is not installed or not on PATH.")
|
|
16
|
+
console.error("Install it from https://bun.sh, then run: bunx @glassly/create-miniapp my-miniapp")
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
process.exit(result.status ?? 1)
|
|
20
|
+
}
|
package/bin/index.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
prefix?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Package names are reverse-DNS: letters, digits, underscores, each segment
|
|
16
|
+
// starting with a letter. The Store rejects anything else, and every miniapp
|
|
17
|
+
// must sit under the developer org's package prefix.
|
|
18
|
+
const PACKAGE_SEGMENT = /^[a-z][a-z0-9_]*$/
|
|
19
|
+
const PACKAGE_PREFIX = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv: string[]): CliFlags {
|
|
22
|
+
const flags: CliFlags = {}
|
|
23
|
+
for (const arg of argv) {
|
|
24
|
+
if (arg.startsWith("--type=")) {
|
|
25
|
+
const value = arg.slice("--type=".length)
|
|
26
|
+
if (value === "display" || value === "camera") {
|
|
27
|
+
flags.type = value
|
|
28
|
+
} else {
|
|
29
|
+
console.error(`Invalid --type value: "${value}". Expected "display" or "camera".`)
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
} else if (arg.startsWith("--prefix=")) {
|
|
33
|
+
flags.prefix = arg.slice("--prefix=".length)
|
|
34
|
+
} else if (!arg.startsWith("-") && !flags.projectName) {
|
|
35
|
+
flags.projectName = arg
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return flags
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toKebabCase(str: string): string {
|
|
42
|
+
return str
|
|
43
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
44
|
+
.replace(/[\s_]+/g, "-")
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toPackageSegment(str: string): string {
|
|
49
|
+
return toKebabCase(str).replace(/-+/g, "_")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateProjectName(name: string): string | undefined {
|
|
53
|
+
if (!name.trim()) return "Project name is required"
|
|
54
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*$/.test(name)) {
|
|
55
|
+
return "Use letters, numbers, dashes, underscores, or spaces (cannot start with a symbol)"
|
|
56
|
+
}
|
|
57
|
+
if (!PACKAGE_SEGMENT.test(toPackageSegment(name))) {
|
|
58
|
+
return "Project name must start with a letter"
|
|
59
|
+
}
|
|
60
|
+
if (fs.existsSync(path.resolve(process.cwd(), name))) {
|
|
61
|
+
return `Directory "${name}" already exists`
|
|
62
|
+
}
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validatePrefix(prefix: string): string | undefined {
|
|
67
|
+
if (!prefix.trim()) return "Package prefix is required"
|
|
68
|
+
if (!PACKAGE_PREFIX.test(prefix)) {
|
|
69
|
+
return "Use reverse-DNS form like com.example: lowercase letters, digits, underscores, at least two segments"
|
|
70
|
+
}
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bailIfCancelled<T>(value: T | symbol): T {
|
|
75
|
+
if (isCancel(value)) {
|
|
76
|
+
cancel("Cancelled.")
|
|
77
|
+
process.exit(0)
|
|
78
|
+
}
|
|
79
|
+
return value as T
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const flags = parseArgs(process.argv.slice(2))
|
|
83
|
+
|
|
84
|
+
intro("create-miniapp")
|
|
85
|
+
|
|
86
|
+
let projectName = flags.projectName
|
|
87
|
+
if (projectName) {
|
|
88
|
+
const err = validateProjectName(projectName)
|
|
89
|
+
if (err) {
|
|
90
|
+
cancel(err)
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
projectName = bailIfCancelled(
|
|
95
|
+
await text({
|
|
96
|
+
message: "What's your project name?",
|
|
97
|
+
placeholder: "my-miniapp",
|
|
98
|
+
validate: validateProjectName,
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let prefix = flags.prefix
|
|
104
|
+
if (prefix) {
|
|
105
|
+
const err = validatePrefix(prefix)
|
|
106
|
+
if (err) {
|
|
107
|
+
cancel(err)
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
prefix = bailIfCancelled(
|
|
112
|
+
await text({
|
|
113
|
+
message: "Package prefix? (your developer org's prefix from the Developer Console)",
|
|
114
|
+
placeholder: "com.example",
|
|
115
|
+
validate: validatePrefix,
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let glassesType = flags.type
|
|
121
|
+
if (!glassesType) {
|
|
122
|
+
glassesType = bailIfCancelled(
|
|
123
|
+
await select<GlassesType>({
|
|
124
|
+
message: "What kind of glasses is this miniapp for?",
|
|
125
|
+
options: [
|
|
126
|
+
{
|
|
127
|
+
value: "display",
|
|
128
|
+
label: "Display glasses",
|
|
129
|
+
hint: "Even Realities G1/G2, Vuzix Z100",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
value: "camera",
|
|
133
|
+
label: "Camera glasses",
|
|
134
|
+
hint: "Mentra Live",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
142
|
+
const templateDir = path.resolve(import.meta.dirname, "..", "template")
|
|
143
|
+
|
|
144
|
+
const packageName = `${prefix}.${toPackageSegment(projectName)}`
|
|
145
|
+
const hardwareRequirements =
|
|
146
|
+
glassesType === "camera"
|
|
147
|
+
? [
|
|
148
|
+
{type: "CAMERA", level: "REQUIRED"},
|
|
149
|
+
{type: "MICROPHONE", level: "REQUIRED"},
|
|
150
|
+
]
|
|
151
|
+
: [
|
|
152
|
+
{type: "DISPLAY", level: "REQUIRED"},
|
|
153
|
+
{type: "MICROPHONE", level: "REQUIRED"},
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
function renderFile(srcPath: string, destPath: string): void {
|
|
157
|
+
if (path.basename(srcPath) === "miniapp.json") {
|
|
158
|
+
const raw = fs.readFileSync(srcPath, "utf-8")
|
|
159
|
+
const manifest = JSON.parse(raw.replace(/\{\{packageName\}\}/g, packageName))
|
|
160
|
+
manifest.hardwareRequirements = hardwareRequirements
|
|
161
|
+
fs.writeFileSync(destPath, JSON.stringify(manifest, null, 2) + "\n")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = fs.readFileSync(srcPath, "utf-8").replace(/\{\{packageName\}\}/g, packageName)
|
|
166
|
+
fs.writeFileSync(destPath, content)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function copyDir(src: string, dest: string): void {
|
|
170
|
+
fs.mkdirSync(dest, {recursive: true})
|
|
171
|
+
for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
|
|
172
|
+
const srcPath = path.join(src, entry.name)
|
|
173
|
+
// npm strips literal `.gitignore` files on publish; the template
|
|
174
|
+
// ships it as `_gitignore` instead and we rename on copy so the
|
|
175
|
+
// scaffolded project starts with a proper `.gitignore`.
|
|
176
|
+
const renamed = entry.name === "_gitignore" ? ".gitignore" : entry.name
|
|
177
|
+
const destPath = path.join(dest, renamed)
|
|
178
|
+
if (entry.isDirectory()) {
|
|
179
|
+
copyDir(srcPath, destPath)
|
|
180
|
+
} else {
|
|
181
|
+
renderFile(srcPath, destPath)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
copyDir(templateDir, targetDir)
|
|
187
|
+
|
|
188
|
+
note(`cd ${projectName}\nbun install\nbun dev`, "Next steps")
|
|
189
|
+
|
|
190
|
+
outro(`Created ${packageName} in ./${projectName}`)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glassly/create-miniapp",
|
|
3
|
+
"version": "0.1.0-beta.7",
|
|
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 — 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,43 @@
|
|
|
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
|
+
bun run pack # production-build, validate the manifest, zip to build/<pkg>-<version>.zip
|
|
33
|
+
bun run typecheck # tsc --noEmit, no bundle emitted
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`pack` is the distributable artifact on its own: `release` builds and packs for
|
|
37
|
+
you, so reach for `pack` when you want the zip without serving it. `typecheck`
|
|
38
|
+
is types only — the unsupported-API checks live in `build`.
|
|
39
|
+
|
|
40
|
+
The Glassly App keeps one dev entry per manifest package name, so you can scan and
|
|
41
|
+
test multiple dev miniapps side by side. It also caches each entry's name and
|
|
42
|
+
icon. Rescanning the same package updates that entry. Use `bun run release` when
|
|
43
|
+
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 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.1",
|
|
10
|
+
"minHostVersion": "0.3.1",
|
|
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,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "glassly dev",
|
|
6
|
+
"build": "bun run build.ts",
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"release": "glassly release",
|
|
9
|
+
"pack": "glassly pack"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@glassly/miniapp": "0.3.1-dev.528",
|
|
13
|
+
"react": "^19.0.0",
|
|
14
|
+
"react-dom": "^19.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@glassly/cli": "^2.0.0-beta.4",
|
|
18
|
+
"@glassly/miniapp-cli": "^0.1.0-beta.4",
|
|
19
|
+
"@types/react": "^19.0.0",
|
|
20
|
+
"@types/react-dom": "^19.0.0",
|
|
21
|
+
"typescript": "^5.6.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -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 JSRuntime",
|
|
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
|
+
}
|