react-pebble 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.
Files changed (50) hide show
  1. package/dist/lib/compiler.cjs +3 -0
  2. package/dist/lib/compiler.cjs.map +1 -0
  3. package/dist/lib/compiler.js +54 -0
  4. package/dist/lib/compiler.js.map +1 -0
  5. package/dist/lib/components.cjs +2 -0
  6. package/dist/lib/components.cjs.map +1 -0
  7. package/dist/lib/components.js +80 -0
  8. package/dist/lib/components.js.map +1 -0
  9. package/dist/lib/hooks.cjs +2 -0
  10. package/dist/lib/hooks.cjs.map +1 -0
  11. package/dist/lib/hooks.js +99 -0
  12. package/dist/lib/hooks.js.map +1 -0
  13. package/dist/lib/index.cjs +2 -0
  14. package/dist/lib/index.cjs.map +1 -0
  15. package/dist/lib/index.js +585 -0
  16. package/dist/lib/index.js.map +1 -0
  17. package/dist/lib/platform.cjs +2 -0
  18. package/dist/lib/platform.cjs.map +1 -0
  19. package/dist/lib/platform.js +52 -0
  20. package/dist/lib/platform.js.map +1 -0
  21. package/dist/lib/plugin.cjs +60 -0
  22. package/dist/lib/plugin.cjs.map +1 -0
  23. package/dist/lib/plugin.js +102 -0
  24. package/dist/lib/plugin.js.map +1 -0
  25. package/dist/lib/src/compiler/index.d.ts +40 -0
  26. package/dist/lib/src/components/index.d.ts +129 -0
  27. package/dist/lib/src/hooks/index.d.ts +75 -0
  28. package/dist/lib/src/index.d.ts +36 -0
  29. package/dist/lib/src/pebble-dom-shim.d.ts +45 -0
  30. package/dist/lib/src/pebble-dom.d.ts +59 -0
  31. package/dist/lib/src/pebble-output.d.ts +44 -0
  32. package/dist/lib/src/pebble-reconciler.d.ts +16 -0
  33. package/dist/lib/src/pebble-render.d.ts +31 -0
  34. package/dist/lib/src/platform.d.ts +30 -0
  35. package/dist/lib/src/plugin/index.d.ts +20 -0
  36. package/package.json +90 -0
  37. package/scripts/compile-to-piu.ts +1794 -0
  38. package/scripts/deploy.sh +46 -0
  39. package/src/compiler/index.ts +114 -0
  40. package/src/components/index.tsx +280 -0
  41. package/src/hooks/index.ts +311 -0
  42. package/src/index.ts +126 -0
  43. package/src/pebble-dom-shim.ts +266 -0
  44. package/src/pebble-dom.ts +190 -0
  45. package/src/pebble-output.ts +310 -0
  46. package/src/pebble-reconciler.ts +54 -0
  47. package/src/pebble-render.ts +311 -0
  48. package/src/platform.ts +50 -0
  49. package/src/plugin/index.ts +274 -0
  50. package/src/types/moddable.d.ts +156 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * pebble-render.ts — Entry point for react-pebble on Pebble Alloy.
3
+ *
4
+ * Bridges Preact's output (via a DOM-shim over pebble-dom) to Moddable's
5
+ * Poco renderer, which draws into the watch framebuffer. Also hosts the
6
+ * Node mock path used for unit tests and local development.
7
+ *
8
+ * Platform detection is via `typeof screen`:
9
+ * - screen exists → Alloy/XS runtime → real Poco draws
10
+ * - screen undefined → Node → mock Poco records calls to an in-memory log
11
+ */
12
+
13
+ import { options } from 'preact';
14
+ import type { ComponentChild } from 'preact';
15
+ import type Poco from 'commodetto/Poco';
16
+ import type { PocoBitmap, PocoColor, PocoFont } from 'commodetto/Poco';
17
+ import type { DOMElement } from './pebble-dom.js';
18
+ import { PocoRenderer } from './pebble-output.js';
19
+ import type { PebbleButton, PebbleButtonHandler } from './hooks/index.js';
20
+ import { ButtonRegistry } from './hooks/index.js';
21
+ import type { PebbleContainer } from './pebble-reconciler.js';
22
+ import {
23
+ createContainer,
24
+ updateContainer,
25
+ unmountContainer,
26
+ } from './pebble-reconciler.js';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Public types
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface PebblePlatformInfo {
33
+ isReal: boolean;
34
+ platform: 'alloy' | 'mock';
35
+ screenWidth: number;
36
+ screenHeight: number;
37
+ }
38
+
39
+ export interface DrawCall {
40
+ op: string;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface RenderOptions {
45
+ backgroundColor?: string;
46
+ }
47
+
48
+ export interface PebbleApp {
49
+ update(newElement: ComponentChild): void;
50
+ unmount(): void;
51
+ readonly platform: PebblePlatformInfo;
52
+ readonly drawLog: readonly DrawCall[];
53
+ readonly _root: DOMElement;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Mock Poco — records every draw call into a shared log so tests can assert.
58
+ // ---------------------------------------------------------------------------
59
+
60
+ class MockPoco {
61
+ readonly width: number;
62
+ readonly height: number;
63
+ readonly Font: new (name: string, size: number) => PocoFont;
64
+
65
+ constructor(width: number, height: number, private readonly log: DrawCall[]) {
66
+ this.width = width;
67
+ this.height = height;
68
+ const FontImpl = class {
69
+ readonly name: string;
70
+ readonly size: number;
71
+ readonly height: number;
72
+ constructor(name: string, size: number) {
73
+ this.name = name;
74
+ this.size = size;
75
+ this.height = size;
76
+ }
77
+ };
78
+ this.Font = FontImpl as unknown as new (name: string, size: number) => PocoFont;
79
+ }
80
+
81
+ begin(x?: number, y?: number, width?: number, height?: number): void {
82
+ this.log.push({ op: 'begin', x, y, width, height });
83
+ }
84
+ end(): void {
85
+ this.log.push({ op: 'end' });
86
+ }
87
+ continue(x: number, y: number, width: number, height: number): void {
88
+ this.log.push({ op: 'continue', x, y, width, height });
89
+ }
90
+ clip(x?: number, y?: number, width?: number, height?: number): void {
91
+ this.log.push({ op: 'clip', x, y, width, height });
92
+ }
93
+ origin(x?: number, y?: number): void {
94
+ this.log.push({ op: 'origin', x, y });
95
+ }
96
+
97
+ makeColor(r: number, g: number, b: number): PocoColor {
98
+ return ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff);
99
+ }
100
+
101
+ fillRectangle(color: PocoColor, x: number, y: number, width: number, height: number): void {
102
+ this.log.push({ op: 'fillRectangle', color, x, y, width, height });
103
+ }
104
+ blendRectangle(
105
+ color: PocoColor,
106
+ blend: number,
107
+ x: number,
108
+ y: number,
109
+ width: number,
110
+ height: number,
111
+ ): void {
112
+ this.log.push({ op: 'blendRectangle', color, blend, x, y, width, height });
113
+ }
114
+ drawPixel(color: PocoColor, x: number, y: number): void {
115
+ this.log.push({ op: 'drawPixel', color, x, y });
116
+ }
117
+ drawBitmap(_bits: PocoBitmap, x: number, y: number): void {
118
+ this.log.push({ op: 'drawBitmap', x, y });
119
+ }
120
+ drawMonochrome(
121
+ _monochrome: PocoBitmap,
122
+ fore: PocoColor,
123
+ back: PocoColor | undefined,
124
+ x: number,
125
+ y: number,
126
+ ): void {
127
+ this.log.push({ op: 'drawMonochrome', fore, back, x, y });
128
+ }
129
+
130
+ drawText(text: string, font: PocoFont, color: PocoColor, x: number, y: number): void {
131
+ this.log.push({ op: 'drawText', text, font, color, x, y });
132
+ }
133
+ getTextWidth(text: string, font: PocoFont): number {
134
+ const size = (font as unknown as { size?: number }).size ?? 14;
135
+ return Math.round(text.length * size * 0.6);
136
+ }
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Poco construction — real on Alloy, mock in Node
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function createPoco(
144
+ log: DrawCall[],
145
+ pocoCtor: typeof Poco | undefined,
146
+ ): { poco: Poco; info: PebblePlatformInfo } {
147
+ if (typeof screen !== 'undefined' && screen && pocoCtor) {
148
+ const poco = new pocoCtor(screen);
149
+ return {
150
+ poco,
151
+ info: {
152
+ isReal: true,
153
+ platform: 'alloy',
154
+ screenWidth: screen.width,
155
+ screenHeight: screen.height,
156
+ },
157
+ };
158
+ }
159
+
160
+ const width = 200;
161
+ const height = 228;
162
+ const mock = new MockPoco(width, height, log);
163
+ return {
164
+ poco: mock as unknown as Poco,
165
+ info: {
166
+ isReal: false,
167
+ platform: 'mock',
168
+ screenWidth: width,
169
+ screenHeight: height,
170
+ },
171
+ };
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Button wiring — see moddable.d.ts for the known/unknown event names.
176
+ // ---------------------------------------------------------------------------
177
+
178
+ function wireWatchButtons(): () => void {
179
+ if (typeof watch === 'undefined' || !watch) return () => undefined;
180
+
181
+ const normalize = (raw: unknown): PebbleButton | undefined => {
182
+ if (typeof raw !== 'string') return undefined;
183
+ const low = raw.toLowerCase();
184
+ if (low === 'up' || low === 'down' || low === 'select' || low === 'back') {
185
+ return low;
186
+ }
187
+ return undefined;
188
+ };
189
+
190
+ const onShort = (payload?: { button?: unknown }) => {
191
+ const b = normalize(payload?.button);
192
+ if (b) ButtonRegistry.emit(b);
193
+ };
194
+ const onLong = (payload?: { button?: unknown }) => {
195
+ const b = normalize(payload?.button);
196
+ if (b) ButtonRegistry.emit(`long_${b}`);
197
+ };
198
+
199
+ watch.addEventListener('button', onShort);
200
+ watch.addEventListener('buttonClick', onShort);
201
+ watch.addEventListener('longClick', onLong);
202
+
203
+ return () => {
204
+ if (typeof watch === 'undefined' || !watch) return;
205
+ watch.removeEventListener('button', onShort);
206
+ watch.removeEventListener('buttonClick', onShort);
207
+ watch.removeEventListener('longClick', onLong);
208
+ };
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Redraw scheduling
213
+ //
214
+ // Preact renders are synchronous; when any component calls setState, Preact
215
+ // re-runs the diff and mutates the shim tree in place. We hook into that
216
+ // by scheduling a Poco redraw on the next tick.
217
+ // ---------------------------------------------------------------------------
218
+
219
+ function scheduleMicrotask(fn: () => void): void {
220
+ // Prefer Promise.resolve().then for microtask batching; fall back to
221
+ // setTimeout(0) if Promise isn't wired up in some host.
222
+ if (typeof Promise !== 'undefined') {
223
+ Promise.resolve().then(fn);
224
+ } else {
225
+ setTimeout(fn, 0);
226
+ }
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Public render API
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export interface RenderOptionsExt extends RenderOptions {
234
+ /**
235
+ * Pre-imported Poco constructor. Alloy entry files must import Poco at
236
+ * the top and pass it here so the Moddable bundler resolves it correctly.
237
+ */
238
+ poco?: typeof Poco;
239
+ }
240
+
241
+ export function render(element: ComponentChild, options: RenderOptionsExt = {}): PebbleApp {
242
+ const drawLog: DrawCall[] = [];
243
+ const container: PebbleContainer = createContainer();
244
+ const { poco, info } = createPoco(drawLog, options.poco);
245
+ const renderer = new PocoRenderer(poco);
246
+
247
+ let pending = false;
248
+ const redraw = () => {
249
+ pending = false;
250
+ drawLog.length = 0;
251
+ renderer.render(container.pblRoot, { backgroundColor: options.backgroundColor });
252
+ };
253
+
254
+ const schedule = () => {
255
+ if (pending) return;
256
+ pending = true;
257
+ scheduleMicrotask(redraw);
258
+ };
259
+
260
+ // Monkey-patch the shim root to redraw on any mutation.
261
+ // Preact calls appendChild/insertBefore/removeChild/setAttribute on the
262
+ // tree during diff; we only need to schedule a redraw when the diff
263
+ // settles, which is right after the top-level render() call returns.
264
+ // For that we just trigger a redraw synchronously after updateContainer.
265
+
266
+ // Hook Preact's commit phase so hook-driven state updates trigger a redraw
267
+ // without polling. options._commit is an undocumented-but-stable hook that
268
+ // fires once per root-level diff settle.
269
+ type PreactOptionsWithCommit = typeof options & {
270
+ _commit?: (root: unknown, queue: unknown[]) => void;
271
+ __c?: (root: unknown, queue: unknown[]) => void;
272
+ };
273
+ const opts = options as PreactOptionsWithCommit;
274
+ const prevCommit = opts._commit ?? opts.__c;
275
+ const commitHook = (root: unknown, queue: unknown[]) => {
276
+ if (prevCommit) prevCommit(root, queue);
277
+ schedule();
278
+ };
279
+ opts._commit = commitHook;
280
+ opts.__c = commitHook;
281
+
282
+ updateContainer(element, container);
283
+ // Always paint once on mount.
284
+ redraw();
285
+
286
+ // Subscribe to watch events on-device.
287
+ const unwireButtons = wireWatchButtons();
288
+
289
+ return {
290
+ update(newElement) {
291
+ updateContainer(newElement, container);
292
+ schedule();
293
+ },
294
+ unmount() {
295
+ unmountContainer(container);
296
+ // Restore prior commit hook (in case another app was rendered).
297
+ opts._commit = prevCommit;
298
+ opts.__c = prevCommit;
299
+ unwireButtons();
300
+ },
301
+ get platform() {
302
+ return info;
303
+ },
304
+ get drawLog() {
305
+ return drawLog as readonly DrawCall[];
306
+ },
307
+ get _root() {
308
+ return container.pblRoot;
309
+ },
310
+ };
311
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * src/platform.ts — Screen dimensions and platform constants.
3
+ *
4
+ * The compiler sets these before rendering so components can use
5
+ * SCREEN.width / SCREEN.height instead of hardcoding pixel values.
6
+ *
7
+ * Usage:
8
+ * import { SCREEN } from 'react-pebble';
9
+ * <Rect x={0} y={0} w={SCREEN.width} h={SCREEN.height} fill="black" />
10
+ */
11
+
12
+ export interface PebblePlatform {
13
+ name: string;
14
+ width: number;
15
+ height: number;
16
+ isRound: boolean;
17
+ }
18
+
19
+ export const PLATFORMS: Record<string, PebblePlatform> = {
20
+ emery: { name: 'emery', width: 200, height: 228, isRound: false },
21
+ gabbro: { name: 'gabbro', width: 200, height: 228, isRound: false },
22
+ // These are NOT supported by Alloy, but listed for reference/future Rocky.js:
23
+ basalt: { name: 'basalt', width: 144, height: 168, isRound: false },
24
+ chalk: { name: 'chalk', width: 180, height: 180, isRound: true },
25
+ diorite: { name: 'diorite', width: 144, height: 168, isRound: false },
26
+ aplite: { name: 'aplite', width: 144, height: 168, isRound: false },
27
+ };
28
+
29
+ /**
30
+ * Current screen dimensions. Set by the compiler before rendering.
31
+ * Components import this and use SCREEN.width / SCREEN.height for
32
+ * responsive layouts.
33
+ */
34
+ export const SCREEN = {
35
+ width: 200,
36
+ height: 228,
37
+ isRound: false,
38
+ platform: 'emery',
39
+ };
40
+
41
+ /** @internal — called by the compiler to set the platform. */
42
+ export function _setPlatform(platform: string): void {
43
+ const p = PLATFORMS[platform];
44
+ if (p) {
45
+ SCREEN.width = p.width;
46
+ SCREEN.height = p.height;
47
+ SCREEN.isRound = p.isRound;
48
+ SCREEN.platform = p.name;
49
+ }
50
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * src/plugin/index.ts — Vite plugin for react-pebble.
3
+ *
4
+ * Compiles JSX components to piu, scaffolds the Pebble project, and
5
+ * optionally builds + deploys to the emulator. Users add this to their
6
+ * vite.config.ts and run `vite build`.
7
+ *
8
+ * Usage:
9
+ * import { pebblePiu } from 'react-pebble/plugin';
10
+ *
11
+ * export default defineConfig({
12
+ * plugins: [
13
+ * pebblePiu({
14
+ * entry: 'src/App.tsx',
15
+ * settleMs: 200,
16
+ * deploy: true,
17
+ * }),
18
+ * ],
19
+ * });
20
+ */
21
+
22
+ import type { Plugin } from 'vite';
23
+ import { execSync } from 'node:child_process';
24
+ import {
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ writeFileSync,
29
+ } from 'node:fs';
30
+ import { join, resolve, dirname } from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { randomUUID } from 'node:crypto';
33
+ import { compileToPiu } from '../compiler/index.js';
34
+
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+
37
+ export interface PebblePiuOptions {
38
+ /** Path to the entry .tsx file (relative to project root) */
39
+ entry: string;
40
+ /** Milliseconds to wait for async effects before snapshotting */
41
+ settleMs?: number;
42
+ /** Target platform — sets screen dimensions (default: 'emery') */
43
+ platform?: string;
44
+ /** Directory for the generated Pebble project (default: '.pebble-build') */
45
+ buildDir?: string;
46
+ /** Auto-run pebble build + install after compilation */
47
+ deploy?: boolean;
48
+ /** Emulator platform for deploy (default: same as platform) */
49
+ emulator?: string;
50
+ }
51
+
52
+ /**
53
+ * Vite plugin that compiles react-pebble JSX to piu and scaffolds a
54
+ * Pebble project for deployment.
55
+ */
56
+ export function pebblePiu(options: PebblePiuOptions): Plugin {
57
+ const buildDir = resolve(options.buildDir ?? '.pebble-build');
58
+
59
+ return {
60
+ name: 'react-pebble-piu',
61
+ apply: 'build',
62
+
63
+ async closeBundle() {
64
+ const log = (msg: string) => console.log(`[react-pebble] ${msg}`);
65
+
66
+ // 1. Compile JSX → piu
67
+ log(`Compiling ${options.entry}...`);
68
+ const result = await compileToPiu({
69
+ entry: options.entry,
70
+ settleMs: options.settleMs,
71
+ platform: options.platform,
72
+ logger: log,
73
+ });
74
+
75
+ // 2. Scaffold pebble project
76
+ log(`Scaffolding pebble project in ${buildDir}...`);
77
+ scaffoldPebbleProject(buildDir, {
78
+ watchface: !result.hasButtons,
79
+ messageKeys: result.messageKeys,
80
+ });
81
+
82
+ // 3. Write compiled output
83
+ const outputPath = join(buildDir, 'src', 'embeddedjs', 'main.js');
84
+ mkdirSync(dirname(outputPath), { recursive: true });
85
+ writeFileSync(outputPath, result.code);
86
+ log(`Wrote ${result.code.split('\n').length} lines to ${outputPath}`);
87
+
88
+ // 4. Optionally build + deploy
89
+ if (options.deploy) {
90
+ const emu = options.emulator ?? options.platform ?? 'emery';
91
+ log('Running pebble build...');
92
+ try {
93
+ execSync('pebble build', { cwd: buildDir, stdio: 'inherit' });
94
+ log(`Installing to ${emu} emulator...`);
95
+ execSync('pebble kill 2>/dev/null; pebble wipe 2>/dev/null; sleep 2', {
96
+ cwd: buildDir,
97
+ stdio: 'ignore',
98
+ });
99
+ execSync(`pebble install --emulator ${emu}`, {
100
+ cwd: buildDir,
101
+ stdio: 'inherit',
102
+ timeout: 30000,
103
+ });
104
+ log(`Deployed to ${emu}. Run 'cd ${buildDir} && pebble logs' for live output.`);
105
+ } catch (err) {
106
+ log('Deploy failed — is the Pebble SDK installed? (pebble --version)');
107
+ throw err;
108
+ }
109
+ } else {
110
+ log(`Done. To deploy:\n cd ${buildDir} && pebble build && pebble install --emulator emery`);
111
+ }
112
+ },
113
+ };
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Pebble project scaffolding
118
+ // ---------------------------------------------------------------------------
119
+
120
+ interface ScaffoldOptions {
121
+ watchface: boolean;
122
+ messageKeys: string[];
123
+ }
124
+
125
+ function scaffoldPebbleProject(dir: string, options: ScaffoldOptions): void {
126
+ mkdirSync(join(dir, 'src', 'embeddedjs'), { recursive: true });
127
+ mkdirSync(join(dir, 'src', 'c'), { recursive: true });
128
+ mkdirSync(join(dir, 'src', 'pkjs'), { recursive: true });
129
+
130
+ // Preserve UUID across builds (or generate a new one)
131
+ let uuid: string;
132
+ const pkgPath = join(dir, 'package.json');
133
+ if (existsSync(pkgPath)) {
134
+ try {
135
+ const existing = JSON.parse(readFileSync(pkgPath, 'utf-8'));
136
+ uuid = existing?.pebble?.uuid ?? randomUUID();
137
+ } catch {
138
+ uuid = randomUUID();
139
+ }
140
+ } else {
141
+ uuid = randomUUID();
142
+ }
143
+
144
+ // package.json
145
+ const pkg = {
146
+ name: 'react-pebble-app',
147
+ author: 'react-pebble',
148
+ version: '1.0.0',
149
+ keywords: ['pebble-app'],
150
+ private: true,
151
+ dependencies: {},
152
+ pebble: {
153
+ displayName: 'react-pebble-app',
154
+ uuid,
155
+ projectType: 'moddable',
156
+ sdkVersion: '3',
157
+ enableMultiJS: true,
158
+ targetPlatforms: ['emery', 'gabbro'],
159
+ watchapp: { watchface: options.watchface },
160
+ messageKeys: options.messageKeys.length > 0 ? options.messageKeys : ['dummy'],
161
+ resources: { media: [] },
162
+ },
163
+ };
164
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
165
+
166
+ // wscript (static — Pebble SDK build config)
167
+ const wscriptPath = join(dir, 'wscript');
168
+ if (!existsSync(wscriptPath)) {
169
+ writeFileSync(wscriptPath, WSCRIPT_TEMPLATE);
170
+ }
171
+
172
+ // C stub (static)
173
+ const cPath = join(dir, 'src', 'c', 'mdbl.c');
174
+ if (!existsSync(cPath)) {
175
+ writeFileSync(
176
+ cPath,
177
+ `#include <pebble.h>
178
+
179
+ int main(void) {
180
+ Window *w = window_create();
181
+ window_stack_push(w, true);
182
+
183
+ moddable_createMachine(NULL);
184
+
185
+ window_destroy(w);
186
+ }
187
+ `,
188
+ );
189
+ }
190
+
191
+ // Moddable manifest (static)
192
+ const manifestPath = join(dir, 'src', 'embeddedjs', 'manifest.json');
193
+ if (!existsSync(manifestPath)) {
194
+ writeFileSync(
195
+ manifestPath,
196
+ JSON.stringify(
197
+ {
198
+ include: ['$(MODDABLE)/examples/manifest_mod.json'],
199
+ modules: { '*': './main.js' },
200
+ },
201
+ null,
202
+ 2,
203
+ ) + '\n',
204
+ );
205
+ }
206
+
207
+ // Phone-side JS
208
+ const pkjsPath = join(dir, 'src', 'pkjs', 'index.js');
209
+ if (options.messageKeys.length > 0 && options.messageKeys[0] !== 'dummy') {
210
+ writeFileSync(
211
+ pkjsPath,
212
+ `// Phone-side PebbleKit JS — sends data to watch via AppMessage.
213
+ // Replace the mock data below with a real API fetch.
214
+
215
+ Pebble.addEventListener("ready", function () {
216
+ console.log("Phone JS ready.");
217
+ // TODO: fetch real data and send via Pebble.sendAppMessage({ ${options.messageKeys[0]}: jsonString });
218
+ });
219
+ `,
220
+ );
221
+ } else if (!existsSync(pkjsPath)) {
222
+ writeFileSync(
223
+ pkjsPath,
224
+ `Pebble.addEventListener("ready", function(e) {
225
+ console.log("PebbleKit JS ready.");
226
+ });
227
+ `,
228
+ );
229
+ }
230
+ }
231
+
232
+ // Named export only — avoids Vite's MIXED_EXPORTS warning.
233
+ // Users import as: import { pebblePiu } from 'react-pebble/plugin';
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Inline templates (no external file dependencies)
237
+ // ---------------------------------------------------------------------------
238
+
239
+ const WSCRIPT_TEMPLATE = `#
240
+ # Pebble SDK build configuration (auto-generated by react-pebble plugin)
241
+ #
242
+ import os.path
243
+
244
+ top = '.'
245
+ out = 'build'
246
+
247
+ def options(ctx):
248
+ ctx.load('pebble_sdk')
249
+
250
+ def configure(ctx):
251
+ ctx.load('pebble_sdk')
252
+
253
+ def build(ctx):
254
+ ctx.load('pebble_sdk')
255
+ build_worker = os.path.exists('worker_src')
256
+ binaries = []
257
+ cached_env = ctx.env
258
+ for platform in ctx.env.TARGET_PLATFORMS:
259
+ ctx.env = ctx.all_envs[platform]
260
+ ctx.set_group(ctx.env.PLATFORM_NAME)
261
+ app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
262
+ ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf, bin_type='app')
263
+ if build_worker:
264
+ worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
265
+ binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf})
266
+ ctx.pbl_build(source=ctx.path.ant_glob('worker_src/c/**/*.c'), target=worker_elf, bin_type='worker')
267
+ else:
268
+ binaries.append({'platform': platform, 'app_elf': app_elf})
269
+ ctx.env = cached_env
270
+ ctx.set_group('bundle')
271
+ ctx.pbl_bundle(binaries=binaries,
272
+ js=ctx.path.ant_glob(['src/pkjs/**/*.js', 'src/pkjs/**/*.json', 'src/common/**/*.js']),
273
+ js_entry_file='src/pkjs/index.js')
274
+ `;