@rool-dev/app 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +241 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +108 -0
- package/dist/cli/publish.d.ts +9 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +213 -0
- package/dist/cli/vite-utils.d.ts +22 -0
- package/dist/cli/vite-utils.d.ts.map +1 -0
- package/dist/cli/vite-utils.js +96 -0
- package/dist/client.d.ts +79 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +235 -0
- package/dist/dev/AppGrid.svelte +246 -0
- package/dist/dev/AppGrid.svelte.d.ts +14 -0
- package/dist/dev/AppGrid.svelte.d.ts.map +1 -0
- package/dist/dev/DevHostController.d.ts +86 -0
- package/dist/dev/DevHostController.d.ts.map +1 -0
- package/dist/dev/DevHostController.js +395 -0
- package/dist/dev/HostShell.svelte +110 -0
- package/dist/dev/HostShell.svelte.d.ts +11 -0
- package/dist/dev/HostShell.svelte.d.ts.map +1 -0
- package/dist/dev/Sidebar.svelte +223 -0
- package/dist/dev/Sidebar.svelte.d.ts +19 -0
- package/dist/dev/Sidebar.svelte.d.ts.map +1 -0
- package/dist/dev/TabBar.svelte +83 -0
- package/dist/dev/TabBar.svelte.d.ts +14 -0
- package/dist/dev/TabBar.svelte.d.ts.map +1 -0
- package/dist/dev/app.css +1 -0
- package/dist/dev/host-shell.d.ts +8 -0
- package/dist/dev/host-shell.d.ts.map +1 -0
- package/dist/dev/host-shell.js +14807 -0
- package/dist/dev/host-shell.js.map +1 -0
- package/dist/dev/vite-env.d.ts +4 -0
- package/dist/host.d.ts +54 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +171 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +10 -0
- package/dist/protocol.d.ts +46 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/reactive.svelte.d.ts +100 -0
- package/dist/reactive.svelte.d.ts.map +1 -0
- package/dist/reactive.svelte.js +267 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Rool App
|
|
2
|
+
|
|
3
|
+
Build sandboxed apps that run inside Rool Spaces. An app is a Svelte 5 component hosted in an iframe, communicating with the host via a postMessage bridge.
|
|
4
|
+
|
|
5
|
+
Apps are small, standardized, and easy to generate. An app project is just two files:
|
|
6
|
+
|
|
7
|
+
- **`App.svelte`** — Your UI component (receives a reactive channel as a prop)
|
|
8
|
+
- **`rool-app.json`** — Manifest with id, name, and collection access
|
|
9
|
+
|
|
10
|
+
Everything else (Vite config, entry point, HTML, Tailwind CSS) is provided by the CLI.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx rool-app init my-app
|
|
16
|
+
cd my-app
|
|
17
|
+
pnpm install
|
|
18
|
+
npx rool-app dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This opens a dev host at `/__rool-host/` that loads your app in a sandboxed iframe, connected to a real Rool Space.
|
|
22
|
+
|
|
23
|
+
## Manifest
|
|
24
|
+
|
|
25
|
+
`rool-app.json` declares your app's identity and collection access:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"id": "my-app",
|
|
30
|
+
"name": "My App",
|
|
31
|
+
"description": "What this app does",
|
|
32
|
+
"collections": {
|
|
33
|
+
"write": {
|
|
34
|
+
"task": [
|
|
35
|
+
{ "name": "title", "type": { "kind": "string" } },
|
|
36
|
+
{ "name": "done", "type": { "kind": "boolean" } }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"read": "*"
|
|
40
|
+
},
|
|
41
|
+
"systemInstruction": "Optional system instruction for the AI"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Field | Required | Description |
|
|
46
|
+
|-------|----------|-------------|
|
|
47
|
+
| `id` | Yes | Unique identifier (lowercase, hyphens) |
|
|
48
|
+
| `name` | Yes | Display name |
|
|
49
|
+
| `description` | No | Short description |
|
|
50
|
+
| `collections` | No | Collection access declarations (see below) |
|
|
51
|
+
| `systemInstruction` | No | Default system instruction for the AI channel |
|
|
52
|
+
|
|
53
|
+
### Collection Access
|
|
54
|
+
|
|
55
|
+
The `collections` field declares what collections the app works with, grouped by access level:
|
|
56
|
+
|
|
57
|
+
- **`write`** — Collections the app can create, update, and delete objects in. An object with field definitions creates the collection in the space. `"*"` grants write access to all collections.
|
|
58
|
+
- **`read`** — Collections the app can read from. An object with field definitions declares the expected shape. `"*"` grants read access to all collections.
|
|
59
|
+
|
|
60
|
+
`write` implies `read` — no need to list a collection under both.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
// App with its own collections + read access to everything else
|
|
64
|
+
"collections": {
|
|
65
|
+
"write": {
|
|
66
|
+
"card": [
|
|
67
|
+
{ "name": "front", "type": { "kind": "string" } },
|
|
68
|
+
{ "name": "back", "type": { "kind": "string" } }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"read": "*"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Full access to all collections (chat, SQL interface, etc.)
|
|
75
|
+
"collections": {
|
|
76
|
+
"write": "*"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Read-only access to all collections
|
|
80
|
+
"collections": {
|
|
81
|
+
"read": "*"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## App Component
|
|
86
|
+
|
|
87
|
+
`App.svelte` receives a single prop — a `ReactiveAppChannel`:
|
|
88
|
+
|
|
89
|
+
```svelte
|
|
90
|
+
<script lang="ts">
|
|
91
|
+
import type { ReactiveAppChannel } from '@rool-dev/app';
|
|
92
|
+
|
|
93
|
+
interface Props {
|
|
94
|
+
channel: ReactiveAppChannel;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let { channel }: Props = $props();
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<div>
|
|
101
|
+
<p>Connected to: {channel.spaceName}</p>
|
|
102
|
+
<p>Objects: {channel.objectIds.length}</p>
|
|
103
|
+
<button onclick={() => channel.prompt('Hello')}>Send</button>
|
|
104
|
+
</div>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
## ReactiveAppChannel
|
|
110
|
+
|
|
111
|
+
The channel is the app's interface to the host Space. It mirrors the `@rool-dev/svelte` ReactiveChannel API over a postMessage bridge.
|
|
112
|
+
|
|
113
|
+
### Reactive State
|
|
114
|
+
|
|
115
|
+
These are Svelte 5 `$state` properties — use them directly in templates or `$effect` blocks:
|
|
116
|
+
|
|
117
|
+
| Property | Type | Description |
|
|
118
|
+
|----------|------|-------------|
|
|
119
|
+
| `interactions` | `Interaction[]` | Channel interaction history (auto-updates) |
|
|
120
|
+
| `objectIds` | `string[]` | All object IDs in the space (auto-updates on create/delete) |
|
|
121
|
+
|
|
122
|
+
### Properties
|
|
123
|
+
|
|
124
|
+
| Property | Type | Description |
|
|
125
|
+
|----------|------|-------------|
|
|
126
|
+
| `channelId` | `string` | Channel ID |
|
|
127
|
+
| `spaceId` | `string` | Space ID |
|
|
128
|
+
| `spaceName` | `string` | Space name |
|
|
129
|
+
| `role` | `RoolUserRole` | User's role (`owner`, `admin`, `editor`, `viewer`) |
|
|
130
|
+
| `linkAccess` | `LinkAccess` | URL sharing level |
|
|
131
|
+
| `userId` | `string` | Current user's ID |
|
|
132
|
+
| `isReadOnly` | `boolean` | True if viewer role |
|
|
133
|
+
|
|
134
|
+
### Object Operations
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
await channel.getObject(id)
|
|
138
|
+
await channel.findObjects({ where: { type: 'note' } })
|
|
139
|
+
await channel.createObject({ data: { type: 'note', text: '{{expand this}}' } })
|
|
140
|
+
await channel.updateObject(id, { data: { text: 'Updated' } })
|
|
141
|
+
await channel.updateObject(id, { prompt: 'Make it shorter' })
|
|
142
|
+
await channel.deleteObjects([id])
|
|
143
|
+
channel.getObjectIds({ limit: 10, order: 'desc' })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See the [SDK docs](../sdk/README.md) for full details on object operations, `{{placeholder}}` syntax, and `findObjects` options.
|
|
147
|
+
|
|
148
|
+
### AI
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const { message, objects } = await channel.prompt('Create three tasks');
|
|
152
|
+
const { message } = await channel.prompt('Summarize', { readOnly: true, effort: 'QUICK' });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Option | Description |
|
|
156
|
+
|--------|-------------|
|
|
157
|
+
| `objectIds` | Focus on specific objects |
|
|
158
|
+
| `responseSchema` | Request structured JSON response |
|
|
159
|
+
| `effort` | `'QUICK'`, `'STANDARD'`, `'REASONING'`, or `'RESEARCH'` |
|
|
160
|
+
| `ephemeral` | Don't record in interaction history |
|
|
161
|
+
| `readOnly` | Disable mutation tools |
|
|
162
|
+
|
|
163
|
+
### Schema
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
channel.getSchema()
|
|
167
|
+
await channel.createCollection('task', [
|
|
168
|
+
{ name: 'title', type: { kind: 'string' } },
|
|
169
|
+
{ name: 'done', type: { kind: 'boolean' } },
|
|
170
|
+
])
|
|
171
|
+
await channel.alterCollection('task', [...updatedFields])
|
|
172
|
+
await channel.dropCollection('task')
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Undo/Redo
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
await channel.checkpoint('Before delete')
|
|
179
|
+
await channel.deleteObjects([id])
|
|
180
|
+
await channel.undo() // restores deleted object
|
|
181
|
+
await channel.redo() // deletes again
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Metadata
|
|
185
|
+
|
|
186
|
+
Arbitrary key-value storage on the Space (not visible to AI):
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
channel.setMetadata('viewport', { zoom: 1.5 })
|
|
190
|
+
channel.getMetadata('viewport')
|
|
191
|
+
channel.getAllMetadata()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Interaction History
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
channel.getInteractions()
|
|
198
|
+
channel.getSystemInstruction()
|
|
199
|
+
await channel.setSystemInstruction('Respond in haiku')
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Events
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
channel.on('objectCreated', ({ objectId, object, source }) => { ... })
|
|
206
|
+
channel.on('objectUpdated', ({ objectId, object, source }) => { ... })
|
|
207
|
+
channel.on('objectDeleted', ({ objectId, source }) => { ... })
|
|
208
|
+
channel.on('metadataUpdated', ({ metadata, source }) => { ... })
|
|
209
|
+
channel.on('channelUpdated', ({ channelId, source }) => { ... })
|
|
210
|
+
channel.on('reset', ({ source }) => { ... })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`source` is `'local_user'`, `'remote_user'`, `'remote_agent'`, or `'system'`.
|
|
214
|
+
|
|
215
|
+
### Reactive Primitives
|
|
216
|
+
|
|
217
|
+
#### `channel.watch(options)`
|
|
218
|
+
|
|
219
|
+
Auto-updating filtered object list:
|
|
220
|
+
|
|
221
|
+
```svelte
|
|
222
|
+
<script>
|
|
223
|
+
const tasks = channel.watch({ where: { type: 'task' } });
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
{#each tasks.objects as task}
|
|
227
|
+
<div>{task.title}</div>
|
|
228
|
+
{/each}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| State | Description |
|
|
232
|
+
|-------|-------------|
|
|
233
|
+
| `watch.objects` | `$state<RoolObject[]>` — matching objects |
|
|
234
|
+
| `watch.loading` | `$state<boolean>` — loading state |
|
|
235
|
+
|
|
236
|
+
Methods: `watch.refresh()`, `watch.close()`.
|
|
237
|
+
|
|
238
|
+
#### `channel.object(id)`
|
|
239
|
+
|
|
240
|
+
Single reactive object subscription:
|
|
241
|
+
|
|
242
|
+
```svelte
|
|
243
|
+
<script>
|
|
244
|
+
const item = channel.object('abc123');
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
{#if item.data}
|
|
248
|
+
<div>{item.data.title}</div>
|
|
249
|
+
{/if}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| State | Description |
|
|
253
|
+
|-------|-------------|
|
|
254
|
+
| `object.data` | `$state<RoolObject | undefined>` — object data |
|
|
255
|
+
| `object.loading` | `$state<boolean>` — loading state |
|
|
256
|
+
|
|
257
|
+
Methods: `object.refresh()`, `object.close()`.
|
|
258
|
+
|
|
259
|
+
## Hosting
|
|
260
|
+
|
|
261
|
+
Apps 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 app never authenticates directly — the host handles auth and forwards operations.
|
|
262
|
+
|
|
263
|
+
The bridge protocol:
|
|
264
|
+
1. App sends `rool:ready`
|
|
265
|
+
2. Host responds with `rool:init` (channel metadata, schema, space info)
|
|
266
|
+
3. App calls channel methods → `rool:request` → host executes → `rool:response`
|
|
267
|
+
4. Host pushes real-time events → `rool:event` → app updates reactive state
|
|
268
|
+
|
|
269
|
+
## CLI Commands
|
|
270
|
+
|
|
271
|
+
| Command | Description |
|
|
272
|
+
|---------|-------------|
|
|
273
|
+
| `rool-app init [name]` | Scaffold a new app project |
|
|
274
|
+
| `rool-app dev` | Start the dev server with host shell |
|
|
275
|
+
|
|
276
|
+
## Exported Types
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import type {
|
|
280
|
+
ReactiveAppChannel,
|
|
281
|
+
ReactiveObject,
|
|
282
|
+
ReactiveWatch,
|
|
283
|
+
WatchOptions,
|
|
284
|
+
RoolObject,
|
|
285
|
+
RoolObjectStat,
|
|
286
|
+
SpaceSchema,
|
|
287
|
+
CollectionDef,
|
|
288
|
+
FieldDef,
|
|
289
|
+
FieldType,
|
|
290
|
+
Interaction,
|
|
291
|
+
ToolCall,
|
|
292
|
+
PromptOptions,
|
|
293
|
+
PromptEffort,
|
|
294
|
+
FindObjectsOptions,
|
|
295
|
+
CreateObjectOptions,
|
|
296
|
+
UpdateObjectOptions,
|
|
297
|
+
ChangeSource,
|
|
298
|
+
RoolUserRole,
|
|
299
|
+
LinkAccess,
|
|
300
|
+
AppChannelEvents,
|
|
301
|
+
} from '@rool-dev/app';
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT - see [LICENSE](../../LICENSE) for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-app dev
|
|
3
|
+
*
|
|
4
|
+
* Starts the app's Vite dev server with the dev host shell injected.
|
|
5
|
+
* The host shell is served at /__rool-host/ and the app at /.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx rool-app dev
|
|
8
|
+
*/
|
|
9
|
+
import { createServer } from 'vite';
|
|
10
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
11
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
12
|
+
import { readFileSync, existsSync, watch } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { readManifest, getSvelteAliases } from './vite-utils.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Paths
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const HOST_SHELL_JS_PATH = resolve(__dirname, '../dev/host-shell.js');
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// HTML generation
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
function escapeHtml(s) {
|
|
25
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
26
|
+
}
|
|
27
|
+
function generateHostHtml(result) {
|
|
28
|
+
const { manifest, error } = result;
|
|
29
|
+
const channelId = manifest?.id ?? 'app-dev';
|
|
30
|
+
const dataAttrs = {
|
|
31
|
+
'data-channel-id': channelId,
|
|
32
|
+
'data-app-url': '/',
|
|
33
|
+
};
|
|
34
|
+
if (manifest)
|
|
35
|
+
dataAttrs['data-manifest'] = escapeHtml(JSON.stringify(manifest));
|
|
36
|
+
if (error)
|
|
37
|
+
dataAttrs['data-manifest-error'] = escapeHtml(error);
|
|
38
|
+
const attrs = Object.entries(dataAttrs).map(([k, v]) => `${k}="${v}"`).join('\n ');
|
|
39
|
+
return `<!DOCTYPE html>
|
|
40
|
+
<html lang="en" style="height:100%">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="UTF-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
|
+
<title>${manifest?.name ? escapeHtml(manifest.name) + ' \u2014 ' : ''}App Dev Host</title>
|
|
45
|
+
<script type="module" src="/@vite/client"></script>
|
|
46
|
+
</head>
|
|
47
|
+
<body style="height:100%;margin:0">
|
|
48
|
+
<div id="rool-host"
|
|
49
|
+
style="display:flex;height:100%;background:#f8fafc"
|
|
50
|
+
${attrs}
|
|
51
|
+
></div>
|
|
52
|
+
<script type="module" src="/__rool-host/host-shell.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>`;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Vite plugins (internal — injected by the CLI, not user-facing)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* Synthesizes index.html and the app entry module so app projects
|
|
61
|
+
* only need App.svelte + rool-app.json.
|
|
62
|
+
*/
|
|
63
|
+
function roolAppPlugin(root, tailwindCssPath) {
|
|
64
|
+
const VIRTUAL_ENTRY = 'virtual:rool-app-entry';
|
|
65
|
+
const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY;
|
|
66
|
+
const VIRTUAL_CSS = 'virtual:rool-app-tailwind.css';
|
|
67
|
+
const RESOLVED_CSS = '\0' + VIRTUAL_CSS;
|
|
68
|
+
const appPath = resolve(root, 'App.svelte');
|
|
69
|
+
const cssPath = resolve(root, 'app.css');
|
|
70
|
+
const hasCss = existsSync(cssPath);
|
|
71
|
+
return {
|
|
72
|
+
name: 'rool-app-entry',
|
|
73
|
+
resolveId(id) {
|
|
74
|
+
if (id === VIRTUAL_ENTRY)
|
|
75
|
+
return RESOLVED_ENTRY;
|
|
76
|
+
if (id === VIRTUAL_CSS)
|
|
77
|
+
return RESOLVED_CSS;
|
|
78
|
+
return undefined;
|
|
79
|
+
},
|
|
80
|
+
load(id) {
|
|
81
|
+
if (id === RESOLVED_CSS)
|
|
82
|
+
return `@import "${tailwindCssPath}";`;
|
|
83
|
+
if (id !== RESOLVED_ENTRY)
|
|
84
|
+
return;
|
|
85
|
+
return [
|
|
86
|
+
`import { initApp } from '@rool-dev/app';`,
|
|
87
|
+
`import { mount } from 'svelte';`,
|
|
88
|
+
`import App from '${appPath}';`,
|
|
89
|
+
`import '${VIRTUAL_CSS}';`,
|
|
90
|
+
hasCss ? `import '${cssPath}';` : ``,
|
|
91
|
+
``,
|
|
92
|
+
`async function main() {`,
|
|
93
|
+
` const channel = await initApp();`,
|
|
94
|
+
` mount(App, {`,
|
|
95
|
+
` target: document.getElementById('app'),`,
|
|
96
|
+
` props: { channel },`,
|
|
97
|
+
` });`,
|
|
98
|
+
`}`,
|
|
99
|
+
``,
|
|
100
|
+
`main().catch((err) => {`,
|
|
101
|
+
` document.getElementById('app').innerHTML =`,
|
|
102
|
+
` '<div style="padding:2rem;color:red"><h2>Failed to initialize app</h2><p>' + err.message + '</p></div>';`,
|
|
103
|
+
`});`,
|
|
104
|
+
].filter(Boolean).join('\n');
|
|
105
|
+
},
|
|
106
|
+
configureServer(server) {
|
|
107
|
+
server.middlewares.use((req, res, next) => {
|
|
108
|
+
// Serve synthesized index.html at /
|
|
109
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
110
|
+
const html = `<!DOCTYPE html>
|
|
111
|
+
<html lang="en" style="height:100%">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="UTF-8">
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
115
|
+
<title>App</title>
|
|
116
|
+
</head>
|
|
117
|
+
<body style="height:100%;margin:0">
|
|
118
|
+
<div id="app" style="height:100%"></div>
|
|
119
|
+
<script type="module" src="/@id/${VIRTUAL_ENTRY}"></script>
|
|
120
|
+
</body>
|
|
121
|
+
</html>`;
|
|
122
|
+
server.transformIndexHtml(req.url, html).then((transformed) => {
|
|
123
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
124
|
+
res.end(transformed);
|
|
125
|
+
}).catch(next);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
next();
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function roolHostPlugin(state, hostShellJs) {
|
|
134
|
+
return {
|
|
135
|
+
name: 'rool-app-host',
|
|
136
|
+
configureServer(server) {
|
|
137
|
+
server.middlewares.use((req, res, next) => {
|
|
138
|
+
if (!req.url?.startsWith('/__rool-host'))
|
|
139
|
+
return next();
|
|
140
|
+
if (req.url === '/__rool-host/host-shell.js') {
|
|
141
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
142
|
+
res.end(hostShellJs);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const html = generateHostHtml(state.current);
|
|
146
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
147
|
+
res.end(html);
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Manifest file watcher
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
function watchManifest(root, state, server) {
|
|
156
|
+
const manifestPath = resolve(root, 'rool-app.json');
|
|
157
|
+
let debounce = null;
|
|
158
|
+
const onChange = () => {
|
|
159
|
+
if (debounce)
|
|
160
|
+
clearTimeout(debounce);
|
|
161
|
+
debounce = setTimeout(() => {
|
|
162
|
+
const prev = state.current;
|
|
163
|
+
state.current = readManifest(root);
|
|
164
|
+
if (JSON.stringify(prev) === JSON.stringify(state.current))
|
|
165
|
+
return;
|
|
166
|
+
if (state.current.error) {
|
|
167
|
+
console.warn(`\n \u26a0 Manifest: ${state.current.error}\n`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(`\n \u2713 Manifest updated \u2014 ${state.current.manifest.name}\n`);
|
|
171
|
+
}
|
|
172
|
+
server.ws.send({ type: 'full-reload', path: '*' });
|
|
173
|
+
}, 100);
|
|
174
|
+
};
|
|
175
|
+
// Watch the file (and parent dir so we catch creation/deletion)
|
|
176
|
+
try {
|
|
177
|
+
watch(manifestPath, onChange);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// File may not exist yet — watch the directory instead
|
|
181
|
+
}
|
|
182
|
+
watch(root, (_, filename) => {
|
|
183
|
+
if (filename === 'rool-app.json')
|
|
184
|
+
onChange();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Main
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
async function main() {
|
|
191
|
+
const cwd = process.cwd();
|
|
192
|
+
const state = { current: readManifest(cwd) };
|
|
193
|
+
if (state.current.error) {
|
|
194
|
+
console.warn(`\n \u26a0 Manifest: ${state.current.error}\n`);
|
|
195
|
+
}
|
|
196
|
+
// Load pre-built host shell bundle
|
|
197
|
+
let hostShellJs;
|
|
198
|
+
try {
|
|
199
|
+
hostShellJs = readFileSync(HOST_SHELL_JS_PATH, 'utf-8');
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
console.error(`Could not find host-shell.js at ${HOST_SHELL_JS_PATH}.\n` +
|
|
203
|
+
`Run "pnpm build" in the @rool-dev/app package first.`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
// Resolve packages from the CLI's own node_modules so apps don't need them
|
|
207
|
+
// and to ensure a single copy of svelte (compiler + runtime must match)
|
|
208
|
+
const tailwindPkgDir = dirname(fileURLToPath(import.meta.resolve('tailwindcss/package.json')));
|
|
209
|
+
const tailwindCssPath = resolve(tailwindPkgDir, 'index.css');
|
|
210
|
+
const appPkgPath = resolve(__dirname, '..');
|
|
211
|
+
const server = await createServer({
|
|
212
|
+
configFile: false,
|
|
213
|
+
root: cwd,
|
|
214
|
+
server: {
|
|
215
|
+
open: '/__rool-host/',
|
|
216
|
+
},
|
|
217
|
+
resolve: {
|
|
218
|
+
alias: [
|
|
219
|
+
{ find: '@rool-dev/app', replacement: appPkgPath },
|
|
220
|
+
{ find: /^tailwindcss$/, replacement: tailwindCssPath },
|
|
221
|
+
...getSvelteAliases(),
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
plugins: [
|
|
225
|
+
tailwindcss(),
|
|
226
|
+
svelte(),
|
|
227
|
+
roolAppPlugin(cwd, tailwindCssPath),
|
|
228
|
+
roolHostPlugin(state, hostShellJs),
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
await server.listen();
|
|
232
|
+
server.printUrls();
|
|
233
|
+
const name = state.current.manifest?.name ?? 'app';
|
|
234
|
+
console.log(`\n Dev host ready \u2014 serving ${name} via bridge\n`);
|
|
235
|
+
// Start watching the manifest for changes
|
|
236
|
+
watchManifest(cwd, state, server);
|
|
237
|
+
}
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error('Failed to start dev server:', err);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const command = process.argv[2];
|
|
3
|
+
switch (command) {
|
|
4
|
+
case 'dev':
|
|
5
|
+
await import('./dev.js');
|
|
6
|
+
break;
|
|
7
|
+
case 'init':
|
|
8
|
+
await import('./init.js');
|
|
9
|
+
break;
|
|
10
|
+
case 'publish':
|
|
11
|
+
await import('./publish.js');
|
|
12
|
+
break;
|
|
13
|
+
default:
|
|
14
|
+
console.log(`Usage: rool-app <command>
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
init Create a new app project
|
|
18
|
+
dev Start the dev server
|
|
19
|
+
publish Build and publish the app`);
|
|
20
|
+
process.exit(command ? 1 : 0);
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|