@rool-dev/extension 0.3.5

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 (65) hide show
  1. package/README.md +458 -0
  2. package/dist/cli/build-pipeline.d.ts +18 -0
  3. package/dist/cli/build-pipeline.d.ts.map +1 -0
  4. package/dist/cli/build-pipeline.js +160 -0
  5. package/dist/cli/build.d.ts +9 -0
  6. package/dist/cli/build.d.ts.map +1 -0
  7. package/dist/cli/build.js +17 -0
  8. package/dist/cli/dev.d.ts +10 -0
  9. package/dist/cli/dev.d.ts.map +1 -0
  10. package/dist/cli/dev.js +257 -0
  11. package/dist/cli/index.d.ts +3 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +34 -0
  14. package/dist/cli/init.d.ts +8 -0
  15. package/dist/cli/init.d.ts.map +1 -0
  16. package/dist/cli/init.js +113 -0
  17. package/dist/cli/publish.d.ts +9 -0
  18. package/dist/cli/publish.d.ts.map +1 -0
  19. package/dist/cli/publish.js +65 -0
  20. package/dist/cli/vite-utils.d.ts +23 -0
  21. package/dist/cli/vite-utils.d.ts.map +1 -0
  22. package/dist/cli/vite-utils.js +105 -0
  23. package/dist/client.d.ts +139 -0
  24. package/dist/client.d.ts.map +1 -0
  25. package/dist/client.js +360 -0
  26. package/dist/dev/AppGrid.svelte +246 -0
  27. package/dist/dev/AppGrid.svelte.d.ts +14 -0
  28. package/dist/dev/AppGrid.svelte.d.ts.map +1 -0
  29. package/dist/dev/DevHostController.d.ts +85 -0
  30. package/dist/dev/DevHostController.d.ts.map +1 -0
  31. package/dist/dev/DevHostController.js +429 -0
  32. package/dist/dev/HostShell.svelte +119 -0
  33. package/dist/dev/HostShell.svelte.d.ts +11 -0
  34. package/dist/dev/HostShell.svelte.d.ts.map +1 -0
  35. package/dist/dev/Sidebar.svelte +290 -0
  36. package/dist/dev/Sidebar.svelte.d.ts +22 -0
  37. package/dist/dev/Sidebar.svelte.d.ts.map +1 -0
  38. package/dist/dev/TabBar.svelte +83 -0
  39. package/dist/dev/TabBar.svelte.d.ts +14 -0
  40. package/dist/dev/TabBar.svelte.d.ts.map +1 -0
  41. package/dist/dev/app.css +1 -0
  42. package/dist/dev/host-shell.d.ts +8 -0
  43. package/dist/dev/host-shell.d.ts.map +1 -0
  44. package/dist/dev/host-shell.js +15282 -0
  45. package/dist/dev/host-shell.js.map +1 -0
  46. package/dist/dev/vite-env.d.ts +4 -0
  47. package/dist/host.d.ts +55 -0
  48. package/dist/host.d.ts.map +1 -0
  49. package/dist/host.js +203 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +8 -0
  53. package/dist/manifest.d.ts +40 -0
  54. package/dist/manifest.d.ts.map +1 -0
  55. package/dist/manifest.js +11 -0
  56. package/dist/protocol.d.ts +48 -0
  57. package/dist/protocol.d.ts.map +1 -0
  58. package/dist/protocol.js +14 -0
  59. package/dist/reactive.svelte.d.ts +150 -0
  60. package/dist/reactive.svelte.d.ts.map +1 -0
  61. package/dist/reactive.svelte.js +362 -0
  62. package/dist/types.d.ts +139 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +7 -0
  65. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,458 @@
1
+ # Rool Extension
2
+
3
+ An extension is a feature package that adds capabilities to a Rool Space. Extensions are Svelte 5 components hosted in sandboxed iframes, communicating with the host via a postMessage bridge. Each extension gets a reactive channel as its interface to the Space's objects, schema, AI, and real-time events.
4
+
5
+ Developers build extensions to create custom experiences on top of a Space — productivity tools, dashboards, data views, games, or anything else. Multiple extensions can be installed into the same Space, letting users and teams assemble an AI-powered interface that fits exactly how they work.
6
+
7
+ An extension project is just two files:
8
+
9
+ - **`App.svelte`** — Your UI component (receives a reactive channel as a prop)
10
+ - **`manifest.json`** — Manifest with id, name, icon, visibility, and collection access
11
+
12
+ Everything else (Vite config, entry point, HTML, Tailwind CSS) is provided by the CLI.
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ npx rool-extension init my-extension
18
+ cd my-extension
19
+ pnpm install
20
+ npx rool-extension dev
21
+ ```
22
+
23
+ This opens a dev host at `/__rool-host/` that loads your extension in a sandboxed iframe, connected to a real Rool Space.
24
+
25
+ ## Manifest
26
+
27
+ `manifest.json` declares your extension's identity and collection access:
28
+
29
+ ```json
30
+ {
31
+ "id": "my-extension",
32
+ "name": "My Extension",
33
+ "public": false,
34
+ "icon": "icon.png",
35
+ "description": "What this extension does",
36
+ "collections": {
37
+ "write": {
38
+ "task": [
39
+ { "name": "title", "type": { "kind": "string" } },
40
+ { "name": "done", "type": { "kind": "boolean" } }
41
+ ]
42
+ },
43
+ "read": "*"
44
+ },
45
+ "systemInstruction": "Optional system instruction for the AI"
46
+ }
47
+ ```
48
+
49
+ | Field | Required | Description |
50
+ |-------|----------|-------------|
51
+ | `id` | Yes | Unique identifier (lowercase, hyphens) |
52
+ | `name` | Yes | Display name |
53
+ | `public` | Yes | Whether the extension is listed in the public extension directory |
54
+ | `icon` | No | Path to an icon image file relative to the project root (e.g. `"icon.png"`) |
55
+ | `description` | No | Short description |
56
+ | `collections` | Yes | Collection access declarations — can be `{}` (see below) |
57
+ | `systemInstruction` | No | Default system instruction for the AI channel |
58
+
59
+ ### Collection Access
60
+
61
+ The `collections` field declares what collections the extension works with, grouped by access level:
62
+
63
+ - **`write`** — Collections the extension can create, update, and delete objects in. An object with field definitions creates the collection in the space. `"*"` grants write access to all collections.
64
+ - **`read`** — Collections the extension can read from. An object with field definitions declares the expected shape. `"*"` grants read access to all collections.
65
+
66
+ `write` implies `read` — no need to list a collection under both.
67
+
68
+ ```json
69
+ // Extension with its own collections + read access to everything else
70
+ "collections": {
71
+ "write": {
72
+ "card": [
73
+ { "name": "front", "type": { "kind": "string" } },
74
+ { "name": "back", "type": { "kind": "string" } }
75
+ ]
76
+ },
77
+ "read": "*"
78
+ }
79
+
80
+ // Full access to all collections (chat, SQL interface, etc.)
81
+ "collections": {
82
+ "write": "*"
83
+ }
84
+
85
+ // Read-only access to all collections
86
+ "collections": {
87
+ "read": "*"
88
+ }
89
+ ```
90
+
91
+ ## Extension Component
92
+
93
+ App.svelte receives a single prop — a `ReactiveChannel`:
94
+
95
+ ```svelte
96
+ <script lang="ts">
97
+ import type { ReactiveChannel } from '@rool-dev/extension';
98
+
99
+ interface Props {
100
+ channel: ReactiveChannel;
101
+ }
102
+
103
+ let { channel }: Props = $props();
104
+ </script>
105
+
106
+ <div>
107
+ <p>Connected to: {channel.spaceName}</p>
108
+ <p>Objects: {channel.objectIds.length}</p>
109
+ <button onclick={() => channel.prompt('Hello')}>Send</button>
110
+ </div>
111
+ ```
112
+
113
+ The component can import other `.svelte` components and `.ts` files — standard Svelte/TypeScript conventions apply. Tailwind CSS v4 is available out of the box. Add an `app.css` file to include custom styles.
114
+
115
+ ### Example: Task List
116
+
117
+ A complete extension that lets users add tasks, mark them done, and ask the AI to generate tasks from a description. The `watch` primitive keeps the list in sync with the Space in real-time.
118
+
119
+ ```svelte
120
+ <script lang="ts">
121
+ import type { ReactiveChannel } from '@rool-dev/extension';
122
+
123
+ interface Props { channel: ReactiveChannel }
124
+ let { channel }: Props = $props();
125
+
126
+ const tasks = channel.watch({ collection: 'task' });
127
+
128
+ let input = $state('');
129
+
130
+ async function addTask() {
131
+ if (!input.trim()) return;
132
+ await channel.createObject({ data: { title: input, done: false } });
133
+ input = '';
134
+ }
135
+
136
+ async function generate() {
137
+ if (!input.trim()) return;
138
+ await channel.prompt(`Create tasks for: ${input}`);
139
+ input = '';
140
+ }
141
+ </script>
142
+
143
+ <div class="flex gap-2 mb-4">
144
+ <input bind:value={input} placeholder="New task or describe what you need…"
145
+ class="flex-1 border rounded px-2 py-1" onkeydown={(e) => e.key === 'Enter' && addTask()} />
146
+ <button onclick={addTask} class="px-3 py-1 bg-blue-600 text-white rounded">Add</button>
147
+ <button onclick={generate} class="px-3 py-1 bg-violet-600 text-white rounded">AI Generate</button>
148
+ </div>
149
+
150
+ {#each tasks.objects as task (task.id)}
151
+ <label class="flex items-center gap-2 py-1">
152
+ <input type="checkbox" checked={task.done}
153
+ onchange={() => channel.updateObject(task.id, { data: { done: !task.done } })} />
154
+ <span class:line-through={task.done}>{task.title}</span>
155
+ </label>
156
+ {/each}
157
+ ```
158
+
159
+ This example covers the main patterns you'll use in most extensions: `watch` for a live query, `createObject` for direct mutations, `updateObject` for edits, and `prompt` to let the AI create or modify objects on the user's behalf.
160
+
161
+ ## ReactiveChannel
162
+
163
+ The channel is the extension's interface to the host Space — objects, schema, AI, metadata, undo/redo, and real-time events.
164
+
165
+ ### Reactive State
166
+
167
+ These are Svelte 5 `$state` properties — use them directly in templates or `$effect` blocks:
168
+
169
+ | Property | Type | Description |
170
+ |----------|------|-------------|
171
+ | `interactions` | `Interaction[]` | Channel interaction history (auto-updates) |
172
+ | `objectIds` | `string[]` | All object IDs in the space (auto-updates on create/delete) |
173
+ | `collections` | `string[]` | Collection names from the schema (auto-updates) |
174
+ | `conversations` | `ConversationInfo[]` | Conversations in this channel (auto-updates on create/delete/rename) |
175
+
176
+ ### Properties
177
+
178
+ | Property | Type | Description |
179
+ |----------|------|-------------|
180
+ | `channelId` | `string` | Channel ID |
181
+ | `spaceId` | `string` | Space ID |
182
+ | `spaceName` | `string` | Space name |
183
+ | `role` | `RoolUserRole` | User's role (`owner`, `admin`, `editor`, `viewer`) |
184
+ | `linkAccess` | `LinkAccess` | URL sharing level |
185
+ | `userId` | `string` | Current user's ID |
186
+ | `isReadOnly` | `boolean` | True if viewer role |
187
+
188
+ ### Object Operations
189
+
190
+ Objects are plain key/value records. `id` is reserved; everything else is application-defined. References between objects are data fields whose values are object IDs.
191
+
192
+ | Method | Description |
193
+ |--------|-------------|
194
+ | `getObject(id)` | Get object data, or undefined if not found |
195
+ | `findObjects(options)` | Find objects using filters and/or natural language (see below) |
196
+ | `getObjectIds(options?)` | Get all object IDs. Options: `{ limit?, order? }` |
197
+ | `createObject(options)` | Create a new object. Returns `{ object, message }` |
198
+ | `updateObject(id, options)` | Update an existing object. Returns `{ object, message }` |
199
+ | `deleteObjects(ids)` | Delete objects by ID |
200
+ | `stat(id)` | Get audit info (modifiedAt, modifiedBy) from local cache |
201
+
202
+ #### createObject / updateObject
203
+
204
+ ```typescript
205
+ // Create with literal data
206
+ await channel.createObject({ data: { title: 'Hello', status: 'draft' } })
207
+
208
+ // Use {{placeholders}} for AI-generated content
209
+ await channel.createObject({ data: { headline: '{{catchy headline about coffee}}' } })
210
+
211
+ // Update fields directly
212
+ await channel.updateObject(id, { data: { status: 'published' } })
213
+
214
+ // Update via AI instruction
215
+ await channel.updateObject(id, { prompt: 'Make it shorter and more casual' })
216
+
217
+ // Delete a field by setting it to null
218
+ await channel.updateObject(id, { data: { subtitle: null } })
219
+ ```
220
+
221
+ Placeholders are resolved by the AI during the mutation and replaced with concrete values. The `{{...}}` syntax is never stored.
222
+
223
+ **createObject options:** `data` (required, include `id` for a custom ID), `ephemeral`.
224
+ **updateObject options:** `data`, `prompt`, `ephemeral`.
225
+
226
+ #### findObjects
227
+
228
+ - **`where` only** — exact-match filtering, no AI, no credits
229
+ - **`collection` only** — filter by collection name, no AI, no credits
230
+ - **`prompt` only** — AI-powered semantic query over all objects
231
+ - **`where` + `prompt`** — `where` narrows the set first, then AI queries within it
232
+
233
+ ```typescript
234
+ await channel.findObjects({ collection: 'note' })
235
+ await channel.findObjects({ where: { status: 'active' } })
236
+ await channel.findObjects({ collection: 'note', where: { status: 'active' } })
237
+ await channel.findObjects({ prompt: 'notes about climate solutions' })
238
+ await channel.findObjects({ collection: 'note', prompt: 'most urgent', limit: 5 })
239
+ ```
240
+
241
+ Options: `where`, `collection`, `prompt`, `limit`, `objectIds`, `order` (`'asc'` | `'desc'`), `ephemeral`.
242
+
243
+ #### Hidden Fields
244
+
245
+ Fields starting with `_` (e.g., `_ui`) are hidden from AI and ignored by the schema. Use them for UI state, positions, or other data the AI shouldn't see:
246
+
247
+ ```typescript
248
+ await channel.createObject({ data: { title: 'Note', _ui: { x: 100, y: 200 } } })
249
+ ```
250
+
251
+ ### AI
252
+
253
+ ```typescript
254
+ const { message, objects } = await channel.prompt('Create three tasks');
255
+ const { message } = await channel.prompt('Summarize', { readOnly: true, effort: 'QUICK' });
256
+ ```
257
+
258
+ | Option | Description |
259
+ |--------|-------------|
260
+ | `objectIds` | Focus on specific objects |
261
+ | `responseSchema` | Request structured JSON response |
262
+ | `effort` | `'QUICK'`, `'STANDARD'`, `'REASONING'`, or `'RESEARCH'` |
263
+ | `ephemeral` | Don't record in interaction history |
264
+ | `readOnly` | Disable mutation tools |
265
+ | `attachments` | Files to attach (`File`, `Blob`, or `{ data, contentType }`) |
266
+
267
+ The AI automatically receives interaction history, recently modified objects, and any objects passed via `objectIds` as context.
268
+
269
+ ### Schema
270
+
271
+ ```typescript
272
+ channel.getSchema()
273
+ await channel.createCollection('task', [
274
+ { name: 'title', type: { kind: 'string' } },
275
+ { name: 'done', type: { kind: 'boolean' } },
276
+ ])
277
+ await channel.alterCollection('task', [...updatedFields])
278
+ await channel.dropCollection('task')
279
+ ```
280
+
281
+ ### Undo/Redo
282
+
283
+ ```typescript
284
+ await channel.checkpoint('Before delete')
285
+ await channel.deleteObjects([id])
286
+ await channel.undo() // restores deleted object
287
+ await channel.redo() // deletes again
288
+ ```
289
+
290
+ ### Metadata
291
+
292
+ Arbitrary key-value storage on the Space (not visible to AI):
293
+
294
+ ```typescript
295
+ channel.setMetadata('viewport', { zoom: 1.5 })
296
+ channel.getMetadata('viewport')
297
+ channel.getAllMetadata()
298
+ ```
299
+
300
+ ### Interaction History & Conversations
301
+
302
+ ```typescript
303
+ channel.getInteractions()
304
+ channel.getSystemInstruction()
305
+ await channel.setSystemInstruction('Respond in haiku')
306
+
307
+ // List all conversations in this channel
308
+ channel.getConversations()
309
+
310
+ // Delete or rename a conversation
311
+ await channel.deleteConversation('old-thread')
312
+ await channel.renameConversation('Research')
313
+ ```
314
+
315
+ ### Conversation Handles
316
+
317
+ For extensions that need multiple independent interaction threads (e.g., chat with multiple threads), use `channel.conversation()` to get a reactive handle scoped to a specific conversation:
318
+
319
+ ```svelte
320
+ <script>
321
+ const thread = channel.conversation('thread-42');
322
+ </script>
323
+
324
+ <!-- thread.interactions is reactive $state — auto-updates via SSE -->
325
+ {#each thread.interactions as interaction}
326
+ <div>{interaction.output}</div>
327
+ {/each}
328
+
329
+ <button onclick={() => thread.prompt('Hello')}>Send</button>
330
+ ```
331
+
332
+ ```typescript
333
+ // Reactive state
334
+ thread.interactions // $state<Interaction[]> — auto-updates
335
+
336
+ // All conversation-scoped methods
337
+ await thread.prompt('Hello');
338
+ await thread.createObject({ data: { text: 'Hello' } });
339
+ await thread.setSystemInstruction('Respond in haiku');
340
+ await thread.rename('Research Thread');
341
+
342
+ // Cleanup
343
+ thread.close(); // Stop listening for updates
344
+ ```
345
+
346
+ Conversations are auto-created on first interaction — no explicit create step needed. All conversations share one bridge connection. The 50-message cap applies per conversation.
347
+
348
+ ### Events
349
+
350
+ ```typescript
351
+ channel.on('objectCreated', ({ objectId, object, source }) => { ... })
352
+ channel.on('objectUpdated', ({ objectId, object, source }) => { ... })
353
+ channel.on('objectDeleted', ({ objectId, source }) => { ... })
354
+ channel.on('metadataUpdated', ({ metadata, source }) => { ... })
355
+ channel.on('channelUpdated', ({ channelId, source }) => { ... })
356
+ channel.on('conversationUpdated', ({ conversationId, channelId, source }) => { ... })
357
+ channel.on('reset', ({ source }) => { ... })
358
+ ```
359
+
360
+ `source` is `'local_user'`, `'remote_user'`, `'remote_agent'`, or `'system'`.
361
+
362
+ ### Reactive Primitives
363
+
364
+ #### `channel.watch(options)`
365
+
366
+ Auto-updating filtered object list:
367
+
368
+ ```svelte
369
+ <script>
370
+ const tasks = channel.watch({ collection: 'task' });
371
+ </script>
372
+
373
+ {#each tasks.objects as task}
374
+ <div>{task.title}</div>
375
+ {/each}
376
+ ```
377
+
378
+ | State | Description |
379
+ |-------|-------------|
380
+ | `watch.objects` | `$state<RoolObject[]>` — matching objects |
381
+ | `watch.loading` | `$state<boolean>` — loading state |
382
+
383
+ Methods: `watch.refresh()`, `watch.close()`.
384
+
385
+ #### `channel.object(id)`
386
+
387
+ Single reactive object subscription:
388
+
389
+ ```svelte
390
+ <script>
391
+ const item = channel.object('abc123');
392
+ </script>
393
+
394
+ {#if item.data}
395
+ <div>{item.data.title}</div>
396
+ {/if}
397
+ ```
398
+
399
+ | State | Description |
400
+ |-------|-------------|
401
+ | `object.data` | `$state<RoolObject | undefined>` — object data |
402
+ | `object.loading` | `$state<boolean>` — loading state |
403
+
404
+ Methods: `object.refresh()`, `object.close()`.
405
+
406
+ ## Hosting
407
+
408
+ Extensions run in a sandboxed iframe (`allow-scripts allow-same-origin`). The host creates the iframe, establishes a postMessage bridge, and proxies all channel operations to a real Rool Space. The extension never authenticates directly — the host handles auth and forwards operations.
409
+
410
+ The bridge protocol:
411
+ 1. Extension sends `rool:ready`
412
+ 2. Host responds with `rool:init` (channel metadata, schema, space info)
413
+ 3. Extension calls channel methods → `rool:request` → host executes → `rool:response`
414
+ 4. Host pushes real-time events → `rool:event` → extension updates reactive state
415
+
416
+ ## CLI Commands
417
+
418
+ | Command | Description |
419
+ |---------|-------------|
420
+ | `rool-extension init [name]` | Scaffold a new extension project |
421
+ | `rool-extension dev` | Start the dev server with host shell |
422
+ | `rool-extension build` | Build the extension |
423
+ | `rool-extension publish` | Build and publish the extension |
424
+
425
+ ## Exported Types
426
+
427
+ ```typescript
428
+ import type {
429
+ ReactiveChannel,
430
+ ReactiveConversationHandle,
431
+ ReactiveObject,
432
+ ReactiveWatch,
433
+ WatchOptions,
434
+ RoolObject,
435
+ RoolObjectStat,
436
+ SpaceSchema,
437
+ CollectionDef,
438
+ FieldDef,
439
+ FieldType,
440
+ Interaction,
441
+ InteractionStatus,
442
+ ConversationInfo,
443
+ ToolCall,
444
+ PromptOptions,
445
+ PromptEffort,
446
+ FindObjectsOptions,
447
+ CreateObjectOptions,
448
+ UpdateObjectOptions,
449
+ ChangeSource,
450
+ RoolUserRole,
451
+ LinkAccess,
452
+ ChannelEvents,
453
+ } from '@rool-dev/extension';
454
+ ```
455
+
456
+ ## License
457
+
458
+ MIT - see [LICENSE](../../LICENSE) for details.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Extension build pipeline.
3
+ *
4
+ * Runs a Vite production build for a Rool extension project, producing a ready-to-deploy
5
+ * dist/ directory with index.html, compiled assets, and manifest.json.
6
+ */
7
+ import type { Manifest } from '../manifest.js';
8
+ /**
9
+ * Run a Vite production build for a Rool extension.
10
+ * Produces dist/ with compiled assets, index.html, and manifest.json.
11
+ */
12
+ export declare function buildExtension(cwd: string, manifest: Manifest): Promise<{
13
+ outDir: string;
14
+ totalSize: number;
15
+ }>;
16
+ /** Zip the project directory into a Buffer, excluding node_modules and .git. */
17
+ export declare function zipProject(projectDir: string): Promise<Buffer>;
18
+ //# sourceMappingURL=build-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-pipeline.d.ts","sourceRoot":"","sources":["../../src/cli/build-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA8D/C;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA6EpH;AAQD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY9D"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Extension build pipeline.
3
+ *
4
+ * Runs a Vite production build for a Rool extension project, producing a ready-to-deploy
5
+ * dist/ directory with index.html, compiled assets, and manifest.json.
6
+ */
7
+ import { build } from 'vite';
8
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
9
+ import tailwindcss from '@tailwindcss/vite';
10
+ import { existsSync, readdirSync, statSync, copyFileSync, writeFileSync } from 'fs';
11
+ import { resolve, dirname } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import archiver from 'archiver';
14
+ import { getSvelteAliases } from './vite-utils.js';
15
+ // ---------------------------------------------------------------------------
16
+ // Paths
17
+ // ---------------------------------------------------------------------------
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ // ---------------------------------------------------------------------------
20
+ // Vite build plugin (production version of the dev virtual entry)
21
+ // ---------------------------------------------------------------------------
22
+ function roolExtensionBuildPlugin(root, tailwindCssPath) {
23
+ const VIRTUAL_ENTRY = 'virtual:rool-extension-entry';
24
+ const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY;
25
+ const VIRTUAL_CSS = 'virtual:rool-extension-tailwind.css';
26
+ const RESOLVED_CSS = '\0' + VIRTUAL_CSS;
27
+ const appPath = resolve(root, 'App.svelte');
28
+ const cssPath = resolve(root, 'app.css');
29
+ const hasCss = existsSync(cssPath);
30
+ return {
31
+ name: 'rool-extension-build',
32
+ resolveId(id) {
33
+ if (id === VIRTUAL_ENTRY)
34
+ return RESOLVED_ENTRY;
35
+ if (id === VIRTUAL_CSS)
36
+ return RESOLVED_CSS;
37
+ return undefined;
38
+ },
39
+ load(id) {
40
+ if (id === RESOLVED_CSS)
41
+ return `@import "${tailwindCssPath}";`;
42
+ if (id !== RESOLVED_ENTRY)
43
+ return;
44
+ return [
45
+ `import { initExtension } from '@rool-dev/extension';`,
46
+ `import { mount } from 'svelte';`,
47
+ `import App from '${appPath}';`,
48
+ `import '${VIRTUAL_CSS}';`,
49
+ hasCss ? `import '${cssPath}';` : ``,
50
+ ``,
51
+ `async function main() {`,
52
+ ` const channel = await initExtension();`,
53
+ ` mount(App, {`,
54
+ ` target: document.getElementById('app'),`,
55
+ ` props: { channel },`,
56
+ ` });`,
57
+ `}`,
58
+ ``,
59
+ `main().catch((err) => {`,
60
+ ` document.getElementById('app').innerHTML =`,
61
+ ` '<div style="padding:2rem;color:red"><h2>Failed to initialize extension</h2><p>' + err.message + '</p></div>';`,
62
+ `});`,
63
+ ].filter(Boolean).join('\n');
64
+ },
65
+ };
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Build
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * Run a Vite production build for a Rool extension.
72
+ * Produces dist/ with compiled assets, index.html, and manifest.json.
73
+ */
74
+ export async function buildExtension(cwd, manifest) {
75
+ const tailwindPkgDir = dirname(fileURLToPath(import.meta.resolve('tailwindcss/package.json')));
76
+ const tailwindCssPath = resolve(tailwindPkgDir, 'index.css');
77
+ const extensionPkgPath = resolve(__dirname, '..');
78
+ const outDir = resolve(cwd, 'dist');
79
+ await build({
80
+ configFile: false,
81
+ root: cwd,
82
+ build: {
83
+ outDir,
84
+ emptyOutDir: true,
85
+ rolldownOptions: {
86
+ input: 'virtual:rool-extension-entry',
87
+ },
88
+ },
89
+ resolve: {
90
+ alias: [
91
+ { find: '@rool-dev/extension', replacement: extensionPkgPath },
92
+ { find: /^tailwindcss$/, replacement: tailwindCssPath },
93
+ ...getSvelteAliases(),
94
+ ],
95
+ },
96
+ plugins: [
97
+ tailwindcss(),
98
+ svelte(),
99
+ roolExtensionBuildPlugin(cwd, tailwindCssPath),
100
+ ],
101
+ logLevel: 'warn',
102
+ });
103
+ // Copy manifest.json into dist
104
+ copyFileSync(resolve(cwd, 'manifest.json'), resolve(outDir, 'manifest.json'));
105
+ // Copy icon file into dist if specified
106
+ if (manifest.icon) {
107
+ const iconSrc = resolve(cwd, manifest.icon);
108
+ if (existsSync(iconSrc)) {
109
+ copyFileSync(iconSrc, resolve(outDir, manifest.icon));
110
+ }
111
+ }
112
+ // Write index.html (Vite build doesn't generate one from virtual entry)
113
+ const assets = readdirSync(resolve(outDir, 'assets')).filter(f => f.endsWith('.js') || f.endsWith('.css'));
114
+ const jsFiles = assets.filter(f => f.endsWith('.js'));
115
+ const cssFiles = assets.filter(f => f.endsWith('.css'));
116
+ const indexHtml = `<!DOCTYPE html>
117
+ <html lang="en" style="height:100%">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
+ <title>${manifest.name}</title>
122
+ ${cssFiles.map(f => ` <link rel="stylesheet" href="/assets/${f}">`).join('\n')}
123
+ </head>
124
+ <body style="height:100%;margin:0">
125
+ <div id="app" style="height:100%"></div>
126
+ ${jsFiles.map(f => ` <script type="module" src="/assets/${f}"></script>`).join('\n')}
127
+ </body>
128
+ </html>`;
129
+ writeFileSync(resolve(outDir, 'index.html'), indexHtml);
130
+ // Calculate total size
131
+ let totalSize = 0;
132
+ function walkDir(dir) {
133
+ for (const entry of readdirSync(dir)) {
134
+ const full = resolve(dir, entry);
135
+ const stat = statSync(full);
136
+ if (stat.isDirectory())
137
+ walkDir(full);
138
+ else
139
+ totalSize += stat.size;
140
+ }
141
+ }
142
+ walkDir(outDir);
143
+ return { outDir, totalSize };
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Zip
147
+ // ---------------------------------------------------------------------------
148
+ const ZIP_EXCLUDE = ['node_modules/**', '.git/**'];
149
+ /** Zip the project directory into a Buffer, excluding node_modules and .git. */
150
+ export function zipProject(projectDir) {
151
+ return new Promise((resolve, reject) => {
152
+ const archive = archiver('zip', { zlib: { level: 9 } });
153
+ const chunks = [];
154
+ archive.on('data', (chunk) => chunks.push(chunk));
155
+ archive.on('end', () => resolve(Buffer.concat(chunks)));
156
+ archive.on('error', reject);
157
+ archive.glob('**/*', { cwd: projectDir, ignore: ZIP_EXCLUDE, dot: false });
158
+ archive.finalize();
159
+ });
160
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * rool-extension build
3
+ *
4
+ * Builds the extension with Vite without publishing.
5
+ *
6
+ * Usage: npx rool-extension build
7
+ */
8
+ export declare function build(): Promise<void>;
9
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,wBAAsB,KAAK,kBAQ1B"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * rool-extension build
3
+ *
4
+ * Builds the extension with Vite without publishing.
5
+ *
6
+ * Usage: npx rool-extension build
7
+ */
8
+ import { readManifestOrExit, formatBytes } from './vite-utils.js';
9
+ import { buildExtension } from './build-pipeline.js';
10
+ export async function build() {
11
+ const cwd = process.cwd();
12
+ const manifest = readManifestOrExit(cwd);
13
+ console.log(`\n Building ${manifest.name}...\n`);
14
+ const { totalSize } = await buildExtension(cwd, manifest);
15
+ console.log(`\n Build complete — ${formatBytes(totalSize)}`);
16
+ console.log(` Output: dist/\n`);
17
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * rool-extension dev
3
+ *
4
+ * Starts the extension's Vite dev server with the dev host shell injected.
5
+ * The host shell is served at /__rool-host/ and the extension at /.
6
+ *
7
+ * Usage: npx rool-extension dev
8
+ */
9
+ export declare function dev(): Promise<void>;
10
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuNH,wBAAsB,GAAG,kBAuDxB"}