@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.
- package/README.md +458 -0
- package/dist/cli/build-pipeline.d.ts +18 -0
- package/dist/cli/build-pipeline.d.ts.map +1 -0
- package/dist/cli/build-pipeline.js +160 -0
- package/dist/cli/build.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +17 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +257 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +113 -0
- package/dist/cli/publish.d.ts +9 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +65 -0
- package/dist/cli/vite-utils.d.ts +23 -0
- package/dist/cli/vite-utils.d.ts.map +1 -0
- package/dist/cli/vite-utils.js +105 -0
- package/dist/client.d.ts +139 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +360 -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 +85 -0
- package/dist/dev/DevHostController.d.ts.map +1 -0
- package/dist/dev/DevHostController.js +429 -0
- package/dist/dev/HostShell.svelte +119 -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 +290 -0
- package/dist/dev/Sidebar.svelte.d.ts +22 -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 +15282 -0
- package/dist/dev/host-shell.js.map +1 -0
- package/dist/dev/vite-env.d.ts +4 -0
- package/dist/host.d.ts +55 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +203 -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 +40 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +11 -0
- package/dist/protocol.d.ts +48 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/reactive.svelte.d.ts +150 -0
- package/dist/reactive.svelte.d.ts.map +1 -0
- package/dist/reactive.svelte.js +362 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +79 -0
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
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 ?? 'extension-dev';
|
|
30
|
+
const dataAttrs = {
|
|
31
|
+
'data-channel-id': channelId,
|
|
32
|
+
'data-extension-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 ' : ''}Extension 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 extension entry module so extension projects
|
|
61
|
+
* only need App.svelte + manifest.json.
|
|
62
|
+
*/
|
|
63
|
+
function roolExtensionPlugin(root, tailwindCssPath) {
|
|
64
|
+
const VIRTUAL_ENTRY = 'virtual:rool-extension-entry';
|
|
65
|
+
const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY;
|
|
66
|
+
const VIRTUAL_CSS = 'virtual:rool-extension-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-extension-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 { initExtension } from '@rool-dev/extension';`,
|
|
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 initExtension();`,
|
|
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 extension</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>Extension</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, cwd) {
|
|
134
|
+
return {
|
|
135
|
+
name: 'rool-extension-host',
|
|
136
|
+
configureServer(server) {
|
|
137
|
+
server.middlewares.use(async (req, res, next) => {
|
|
138
|
+
if (!req.url?.startsWith('/__rool-host'))
|
|
139
|
+
return next();
|
|
140
|
+
// Build + zip endpoint for in-browser publishing
|
|
141
|
+
if (req.url === '/__rool-host/publish' && req.method === 'POST') {
|
|
142
|
+
try {
|
|
143
|
+
const { buildExtension, zipProject } = await import('./build-pipeline.js');
|
|
144
|
+
const { readManifestOrExit } = await import('./vite-utils.js');
|
|
145
|
+
const manifest = readManifestOrExit(cwd);
|
|
146
|
+
const { totalSize } = await buildExtension(cwd, manifest);
|
|
147
|
+
const zipBuffer = await zipProject(cwd);
|
|
148
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
149
|
+
res.setHeader('X-Total-Size', String(totalSize));
|
|
150
|
+
res.end(zipBuffer);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
154
|
+
res.statusCode = 500;
|
|
155
|
+
res.setHeader('Content-Type', 'application/json');
|
|
156
|
+
res.end(JSON.stringify({ error: msg }));
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (req.url === '/__rool-host/host-shell.js') {
|
|
161
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
162
|
+
res.end(hostShellJs);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const html = generateHostHtml(state.current);
|
|
166
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
167
|
+
res.end(html);
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Manifest file watcher
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
function watchManifest(root, state, server) {
|
|
176
|
+
const manifestPath = resolve(root, 'manifest.json');
|
|
177
|
+
let debounce = null;
|
|
178
|
+
const onChange = () => {
|
|
179
|
+
if (debounce)
|
|
180
|
+
clearTimeout(debounce);
|
|
181
|
+
debounce = setTimeout(() => {
|
|
182
|
+
const prev = state.current;
|
|
183
|
+
state.current = readManifest(root);
|
|
184
|
+
if (JSON.stringify(prev) === JSON.stringify(state.current))
|
|
185
|
+
return;
|
|
186
|
+
if (state.current.error) {
|
|
187
|
+
console.warn(`\n \u26a0 Manifest: ${state.current.error}\n`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(`\n \u2713 Manifest updated \u2014 ${state.current.manifest.name}\n`);
|
|
191
|
+
}
|
|
192
|
+
server.ws.send({ type: 'full-reload', path: '*' });
|
|
193
|
+
}, 100);
|
|
194
|
+
};
|
|
195
|
+
// Watch the file (and parent dir so we catch creation/deletion)
|
|
196
|
+
try {
|
|
197
|
+
watch(manifestPath, onChange);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// File may not exist yet — watch the directory instead
|
|
201
|
+
}
|
|
202
|
+
watch(root, (_, filename) => {
|
|
203
|
+
if (filename === 'manifest.json')
|
|
204
|
+
onChange();
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Main
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
export async function dev() {
|
|
211
|
+
const cwd = process.cwd();
|
|
212
|
+
const state = { current: readManifest(cwd) };
|
|
213
|
+
if (state.current.error) {
|
|
214
|
+
console.warn(`\n \u26a0 Manifest: ${state.current.error}\n`);
|
|
215
|
+
}
|
|
216
|
+
// Load pre-built host shell bundle
|
|
217
|
+
let hostShellJs;
|
|
218
|
+
try {
|
|
219
|
+
hostShellJs = readFileSync(HOST_SHELL_JS_PATH, 'utf-8');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
console.error(`Could not find host-shell.js at ${HOST_SHELL_JS_PATH}.\n` +
|
|
223
|
+
`Run "pnpm build" in the @rool-dev/extension package first.`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
// Resolve packages from the CLI's own node_modules so apps don't need them
|
|
227
|
+
// and to ensure a single copy of svelte (compiler + runtime must match)
|
|
228
|
+
const tailwindPkgDir = dirname(fileURLToPath(import.meta.resolve('tailwindcss/package.json')));
|
|
229
|
+
const tailwindCssPath = resolve(tailwindPkgDir, 'index.css');
|
|
230
|
+
const extensionPkgPath = resolve(__dirname, '..');
|
|
231
|
+
const server = await createServer({
|
|
232
|
+
configFile: false,
|
|
233
|
+
root: cwd,
|
|
234
|
+
server: {
|
|
235
|
+
open: '/__rool-host/',
|
|
236
|
+
},
|
|
237
|
+
resolve: {
|
|
238
|
+
alias: [
|
|
239
|
+
{ find: '@rool-dev/extension', replacement: extensionPkgPath },
|
|
240
|
+
{ find: /^tailwindcss$/, replacement: tailwindCssPath },
|
|
241
|
+
...getSvelteAliases(),
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
plugins: [
|
|
245
|
+
tailwindcss(),
|
|
246
|
+
svelte(),
|
|
247
|
+
roolExtensionPlugin(cwd, tailwindCssPath),
|
|
248
|
+
roolHostPlugin(state, hostShellJs, cwd),
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
await server.listen();
|
|
252
|
+
server.printUrls();
|
|
253
|
+
const name = state.current.manifest?.name ?? 'extension';
|
|
254
|
+
console.log(`\n Dev host ready \u2014 serving ${name} via bridge\n`);
|
|
255
|
+
// Start watching the manifest for changes
|
|
256
|
+
watchManifest(cwd, state, server);
|
|
257
|
+
}
|
|
@@ -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,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const command = process.argv[2];
|
|
3
|
+
switch (command) {
|
|
4
|
+
case 'dev': {
|
|
5
|
+
const { dev } = await import('./dev.js');
|
|
6
|
+
await dev();
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
case 'init': {
|
|
10
|
+
const { init } = await import('./init.js');
|
|
11
|
+
init();
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
case 'build': {
|
|
15
|
+
const { build } = await import('./build.js');
|
|
16
|
+
await build();
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'publish': {
|
|
20
|
+
const { publish } = await import('./publish.js');
|
|
21
|
+
await publish();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
default:
|
|
25
|
+
console.log(`Usage: rool-extension <command>
|
|
26
|
+
|
|
27
|
+
Commands:
|
|
28
|
+
init Create a new extension project
|
|
29
|
+
dev Start the dev server
|
|
30
|
+
build Build the extension
|
|
31
|
+
publish Build and publish the extension`);
|
|
32
|
+
process.exit(command ? 1 : 0);
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH,iBAAS,IAAI,SAsGZ;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-extension init [name]
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a new extension project in the current directory or a named subdirectory.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
|
+
import { resolve, basename, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
function getExtensionSdkVersion() {
|
|
11
|
+
const pkgPath = resolve(__dirname, '../../package.json');
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
13
|
+
return pkg.version;
|
|
14
|
+
}
|
|
15
|
+
function toId(name) {
|
|
16
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
17
|
+
}
|
|
18
|
+
function init() {
|
|
19
|
+
const name = process.argv[3];
|
|
20
|
+
const dir = name ? resolve(process.cwd(), name) : process.cwd();
|
|
21
|
+
const appName = name ?? basename(dir);
|
|
22
|
+
const appId = toId(appName);
|
|
23
|
+
if (name) {
|
|
24
|
+
if (existsSync(dir)) {
|
|
25
|
+
console.error(`Directory "${name}" already exists.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(resolve(dir, 'manifest.json'))) {
|
|
31
|
+
console.error('manifest.json already exists in this directory.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const manifest = {
|
|
35
|
+
id: appId,
|
|
36
|
+
name: appName,
|
|
37
|
+
public: false,
|
|
38
|
+
collections: {},
|
|
39
|
+
};
|
|
40
|
+
const appSvelte = `<script lang="ts">
|
|
41
|
+
import type { ReactiveChannel } from '@rool-dev/extension';
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
channel: ReactiveChannel;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let { channel }: Props = $props();
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="h-full flex items-center justify-center">
|
|
51
|
+
<div class="text-center">
|
|
52
|
+
<h1 class="text-2xl font-bold text-slate-800 mb-2">${appName}</h1>
|
|
53
|
+
<p class="text-slate-500">Edit App.svelte to get started</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
const packageJson = {
|
|
58
|
+
name: appId,
|
|
59
|
+
private: true,
|
|
60
|
+
version: '0.0.0',
|
|
61
|
+
type: 'module',
|
|
62
|
+
scripts: {
|
|
63
|
+
dev: 'rool-extension dev',
|
|
64
|
+
},
|
|
65
|
+
dependencies: {
|
|
66
|
+
'@rool-dev/extension': `^${getExtensionSdkVersion()}`,
|
|
67
|
+
},
|
|
68
|
+
devDependencies: {
|
|
69
|
+
svelte: '^5.0.0',
|
|
70
|
+
typescript: '^5.0.0',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const agentsMd = `# ${appName}
|
|
74
|
+
|
|
75
|
+
This is a Rool Extension — a sandboxed Svelte 5 component that runs inside a Rool Space.
|
|
76
|
+
|
|
77
|
+
## Documentation
|
|
78
|
+
|
|
79
|
+
Read the extension SDK documentation before making changes:
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
cat node_modules/@rool-dev/extension/README.md
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
## Project structure
|
|
86
|
+
|
|
87
|
+
- \`App.svelte\` — Main component (receives \`channel: ReactiveChannel\` as a prop)
|
|
88
|
+
- \`manifest.json\` — Manifest (id, name, collections)
|
|
89
|
+
- \`app.css\` — Optional custom styles (Tailwind v4 is available by default)
|
|
90
|
+
|
|
91
|
+
Additional \`.svelte\` and \`.ts\` files can be imported from \`App.svelte\`.
|
|
92
|
+
|
|
93
|
+
## Dev server
|
|
94
|
+
|
|
95
|
+
\`\`\`
|
|
96
|
+
npx rool-extension dev
|
|
97
|
+
\`\`\`
|
|
98
|
+
`;
|
|
99
|
+
writeFileSync(resolve(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
100
|
+
writeFileSync(resolve(dir, 'App.svelte'), appSvelte);
|
|
101
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
102
|
+
writeFileSync(resolve(dir, 'AGENTS.md'), agentsMd);
|
|
103
|
+
const relDir = name ?? '.';
|
|
104
|
+
console.log(`
|
|
105
|
+
Created extension "${appName}" in ${relDir}/
|
|
106
|
+
|
|
107
|
+
Next steps:
|
|
108
|
+
${name ? `cd ${name}` : ''}
|
|
109
|
+
npm install
|
|
110
|
+
npx rool-extension dev
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
export { init };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-extension publish
|
|
3
|
+
*
|
|
4
|
+
* Builds the extension with Vite and publishes it to the Rool extension platform.
|
|
5
|
+
*
|
|
6
|
+
* Usage: npx rool-extension publish [--env local|dev|prod]
|
|
7
|
+
*/
|
|
8
|
+
export declare function publish(): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/cli/publish.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAoCH,wBAAsB,OAAO,kBAoC5B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-extension publish
|
|
3
|
+
*
|
|
4
|
+
* Builds the extension with Vite and publishes it to the Rool extension platform.
|
|
5
|
+
*
|
|
6
|
+
* Usage: npx rool-extension publish [--env local|dev|prod]
|
|
7
|
+
*/
|
|
8
|
+
import { RoolClient } from '@rool-dev/sdk';
|
|
9
|
+
import { NodeAuthProvider } from '@rool-dev/sdk/node';
|
|
10
|
+
import { ENV_URLS } from '../manifest.js';
|
|
11
|
+
import { readManifestOrExit, formatBytes } from './vite-utils.js';
|
|
12
|
+
import { buildExtension, zipProject } from './build-pipeline.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// CLI args
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function parseArgs() {
|
|
17
|
+
const args = process.argv.slice(3); // after 'publish'
|
|
18
|
+
let env = 'prod';
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
if (args[i] === '--env' && args[i + 1]) {
|
|
21
|
+
const val = args[i + 1];
|
|
22
|
+
if (val !== 'local' && val !== 'dev' && val !== 'prod') {
|
|
23
|
+
console.error(`Invalid environment: ${val}. Use 'local', 'dev', or 'prod'.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
env = val;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { env };
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Main
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export async function publish() {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const { env } = parseArgs();
|
|
38
|
+
const manifest = readManifestOrExit(cwd);
|
|
39
|
+
console.log(`\n Building ${manifest.name}...\n`);
|
|
40
|
+
const { totalSize } = await buildExtension(cwd, manifest);
|
|
41
|
+
const zipBuffer = await zipProject(cwd);
|
|
42
|
+
console.log(`\n Build complete — ${formatBytes(totalSize)}`);
|
|
43
|
+
console.log(` Bundle: ${formatBytes(zipBuffer.length)}\n`);
|
|
44
|
+
// Authenticate
|
|
45
|
+
const urls = ENV_URLS[env];
|
|
46
|
+
const client = new RoolClient({
|
|
47
|
+
baseUrl: urls.baseUrl,
|
|
48
|
+
authUrl: urls.authUrl,
|
|
49
|
+
authProvider: new NodeAuthProvider(),
|
|
50
|
+
});
|
|
51
|
+
if (!await client.isAuthenticated()) {
|
|
52
|
+
console.log(' Opening browser to authenticate...');
|
|
53
|
+
await client.login('Rool Extension CLI');
|
|
54
|
+
}
|
|
55
|
+
// Publish
|
|
56
|
+
console.log(` Publishing ${manifest.id} to ${env}...`);
|
|
57
|
+
const blob = new Blob([new Uint8Array(zipBuffer)], { type: 'application/zip' });
|
|
58
|
+
const result = await client.publishExtension(manifest.id, {
|
|
59
|
+
bundle: blob,
|
|
60
|
+
});
|
|
61
|
+
console.log(`\n Published: ${result.manifest.name}`);
|
|
62
|
+
console.log(` URL: ${result.url}`);
|
|
63
|
+
console.log(` Size: ${formatBytes(result.sizeBytes)}\n`);
|
|
64
|
+
client.destroy();
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Vite utilities for the CLI (dev server and publish).
|
|
3
|
+
*
|
|
4
|
+
* Node.js only — not included in the browser bundle.
|
|
5
|
+
*/
|
|
6
|
+
import type { Manifest, ManifestResult } from '../manifest.js';
|
|
7
|
+
export declare function readManifest(root: string): ManifestResult;
|
|
8
|
+
/**
|
|
9
|
+
* Strict manifest reading for publish — exits on error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readManifestOrExit(root: string): Manifest;
|
|
12
|
+
/**
|
|
13
|
+
* Builds resolve.alias entries that map every `svelte` and `svelte/*` import
|
|
14
|
+
* to the exact file in the CLI's own svelte copy. This ensures the compiler
|
|
15
|
+
* (loaded from the CLI) and the browser runtime always use the same svelte
|
|
16
|
+
* instance — even when the extension lives outside the monorepo.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSvelteAliases(): {
|
|
19
|
+
find: RegExp;
|
|
20
|
+
replacement: string;
|
|
21
|
+
}[];
|
|
22
|
+
export declare function formatBytes(bytes: number): string;
|
|
23
|
+
//# sourceMappingURL=vite-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-utils.d.ts","sourceRoot":"","sources":["../../src/cli/vite-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAM/D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CA0BzD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAOzD;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CAgB1E;AAmBD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Vite utilities for the CLI (dev server and publish).
|
|
3
|
+
*
|
|
4
|
+
* Node.js only — not included in the browser bundle.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { resolve, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Manifest reading
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function readManifest(root) {
|
|
13
|
+
const path = resolve(root, 'manifest.json');
|
|
14
|
+
if (!existsSync(path)) {
|
|
15
|
+
return { manifest: null, error: 'manifest.json not found' };
|
|
16
|
+
}
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = readFileSync(path, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return { manifest: null, error: `Cannot read manifest.json: ${e instanceof Error ? e.message : String(e)}` };
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { manifest: null, error: 'manifest.json contains invalid JSON' };
|
|
30
|
+
}
|
|
31
|
+
const missing = [];
|
|
32
|
+
if (!parsed.id)
|
|
33
|
+
missing.push('id');
|
|
34
|
+
if (!parsed.name)
|
|
35
|
+
missing.push('name');
|
|
36
|
+
if (typeof parsed.public !== 'boolean')
|
|
37
|
+
missing.push('public');
|
|
38
|
+
if (!parsed.collections || typeof parsed.collections !== 'object')
|
|
39
|
+
missing.push('collections');
|
|
40
|
+
if (missing.length > 0) {
|
|
41
|
+
return { manifest: null, error: `manifest.json missing required fields: ${missing.join(', ')}` };
|
|
42
|
+
}
|
|
43
|
+
return { manifest: parsed, error: null };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Strict manifest reading for publish — exits on error.
|
|
47
|
+
*/
|
|
48
|
+
export function readManifestOrExit(root) {
|
|
49
|
+
const result = readManifest(root);
|
|
50
|
+
if (result.error !== null) {
|
|
51
|
+
console.error(result.error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
return result.manifest;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Svelte resolution
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* Builds resolve.alias entries that map every `svelte` and `svelte/*` import
|
|
61
|
+
* to the exact file in the CLI's own svelte copy. This ensures the compiler
|
|
62
|
+
* (loaded from the CLI) and the browser runtime always use the same svelte
|
|
63
|
+
* instance — even when the extension lives outside the monorepo.
|
|
64
|
+
*/
|
|
65
|
+
export function getSvelteAliases() {
|
|
66
|
+
const svelteDir = dirname(fileURLToPath(import.meta.resolve('svelte/package.json')));
|
|
67
|
+
const pkg = JSON.parse(readFileSync(resolve(svelteDir, 'package.json'), 'utf-8'));
|
|
68
|
+
const aliases = [];
|
|
69
|
+
for (const [exportPath, conditions] of Object.entries(pkg.exports)) {
|
|
70
|
+
const file = pickExport(conditions, ['svelte', 'browser', 'default']);
|
|
71
|
+
if (!file)
|
|
72
|
+
continue;
|
|
73
|
+
const specifier = exportPath === '.' ? 'svelte' : 'svelte' + exportPath.slice(1);
|
|
74
|
+
aliases.push({
|
|
75
|
+
find: new RegExp(`^${specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
|
76
|
+
replacement: resolve(svelteDir, file),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return aliases;
|
|
80
|
+
}
|
|
81
|
+
/** Walk a conditional exports value, picking the first matching condition. */
|
|
82
|
+
function pickExport(value, conditions) {
|
|
83
|
+
if (typeof value === 'string')
|
|
84
|
+
return value;
|
|
85
|
+
if (typeof value !== 'object' || value === null)
|
|
86
|
+
return null;
|
|
87
|
+
for (const c of conditions) {
|
|
88
|
+
if (c in value) {
|
|
89
|
+
const r = pickExport(value[c], conditions);
|
|
90
|
+
if (r)
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Formatting
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
export function formatBytes(bytes) {
|
|
100
|
+
if (bytes < 1024)
|
|
101
|
+
return `${bytes} B`;
|
|
102
|
+
if (bytes < 1024 * 1024)
|
|
103
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
104
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
105
|
+
}
|