@lotics/cli 0.27.0 → 0.29.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/dist/src/client.d.ts +30 -0
- package/dist/src/client.js +14 -0
- package/dist/src/dev/rpc_handler.d.ts +1 -1
- package/dist/src/dev/rpc_handler.js +34 -1
- package/dist/src/dev/wrapper_page.js +47 -12
- package/dist/src/starter_template.d.ts +5 -2
- package/dist/src/starter_template.js +22 -5
- package/package.json +1 -1
package/dist/src/client.d.ts
CHANGED
|
@@ -129,6 +129,36 @@ export declare class LoticsClient {
|
|
|
129
129
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
130
130
|
*/
|
|
131
131
|
appWorkflow(app_id: string, alias: string, inputs: unknown): Promise<unknown>;
|
|
132
|
+
/**
|
|
133
|
+
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
134
|
+
* POST /v1/apps/{app_id}/files/upload-url.
|
|
135
|
+
*/
|
|
136
|
+
appRequestFileUpload(app_id: string, body: {
|
|
137
|
+
filename: string;
|
|
138
|
+
mime_type: string;
|
|
139
|
+
file_size: number;
|
|
140
|
+
}): Promise<{
|
|
141
|
+
file_id: string;
|
|
142
|
+
file_storage_key: string;
|
|
143
|
+
upload_url: string;
|
|
144
|
+
}>;
|
|
145
|
+
/**
|
|
146
|
+
* Finalize a presigned upload once the bytes are in storage. Mirrors
|
|
147
|
+
* POST /v1/apps/{app_id}/files/complete.
|
|
148
|
+
*/
|
|
149
|
+
appCompleteFileUpload(app_id: string, body: {
|
|
150
|
+
file_id: string;
|
|
151
|
+
file_storage_key: string;
|
|
152
|
+
filename: string;
|
|
153
|
+
}): Promise<{
|
|
154
|
+
file: {
|
|
155
|
+
id: string;
|
|
156
|
+
filename: string;
|
|
157
|
+
mime_type: string;
|
|
158
|
+
url?: string;
|
|
159
|
+
thumbnail_url?: string;
|
|
160
|
+
};
|
|
161
|
+
}>;
|
|
132
162
|
deployAppVersion(args: {
|
|
133
163
|
app_id: string;
|
|
134
164
|
source_archive: Buffer;
|
package/dist/src/client.js
CHANGED
|
@@ -175,6 +175,20 @@ export class LoticsClient {
|
|
|
175
175
|
async appWorkflow(app_id, alias, inputs) {
|
|
176
176
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/execute`, { inputs });
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
180
|
+
* POST /v1/apps/{app_id}/files/upload-url.
|
|
181
|
+
*/
|
|
182
|
+
async appRequestFileUpload(app_id, body) {
|
|
183
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/files/upload-url`, body);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Finalize a presigned upload once the bytes are in storage. Mirrors
|
|
187
|
+
* POST /v1/apps/{app_id}/files/complete.
|
|
188
|
+
*/
|
|
189
|
+
async appCompleteFileUpload(app_id, body) {
|
|
190
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/files/complete`, body);
|
|
191
|
+
}
|
|
178
192
|
async deployAppVersion(args) {
|
|
179
193
|
const formData = new FormData();
|
|
180
194
|
// Wrap Buffers as Uint8Array views so the Blob constructor accepts them
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
8
|
import { LoticsClient } from "../client.js";
|
|
9
|
-
export type RpcOp = "query" | "workflow";
|
|
9
|
+
export type RpcOp = "query" | "workflow" | "upload_url" | "upload_complete";
|
|
10
10
|
export interface RpcRequest {
|
|
11
11
|
app_id: string;
|
|
12
12
|
op: RpcOp;
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* Errors are thrown — the HTTP server caller serializes them to a 500 with
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
|
-
const SUPPORTED_OPS = new Set([
|
|
8
|
+
const SUPPORTED_OPS = new Set([
|
|
9
|
+
"query",
|
|
10
|
+
"workflow",
|
|
11
|
+
"upload_url",
|
|
12
|
+
"upload_complete",
|
|
13
|
+
]);
|
|
9
14
|
export async function dispatchRpc(client, body) {
|
|
10
15
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
11
16
|
throw new Error("RPC envelope must include app_id and op");
|
|
@@ -28,6 +33,34 @@ export async function dispatchRpc(client, body) {
|
|
|
28
33
|
}
|
|
29
34
|
return client.appWorkflow(body.app_id, p.alias, p.inputs);
|
|
30
35
|
}
|
|
36
|
+
case "upload_url": {
|
|
37
|
+
const p = body.payload;
|
|
38
|
+
if (!p ||
|
|
39
|
+
typeof p.filename !== "string" ||
|
|
40
|
+
typeof p.mime_type !== "string" ||
|
|
41
|
+
typeof p.file_size !== "number") {
|
|
42
|
+
throw new Error("upload_url payload must include filename, mime_type, file_size");
|
|
43
|
+
}
|
|
44
|
+
return client.appRequestFileUpload(body.app_id, {
|
|
45
|
+
filename: p.filename,
|
|
46
|
+
mime_type: p.mime_type,
|
|
47
|
+
file_size: p.file_size,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
case "upload_complete": {
|
|
51
|
+
const p = body.payload;
|
|
52
|
+
if (!p ||
|
|
53
|
+
typeof p.file_id !== "string" ||
|
|
54
|
+
typeof p.file_storage_key !== "string" ||
|
|
55
|
+
typeof p.filename !== "string") {
|
|
56
|
+
throw new Error("upload_complete payload must include file_id, file_storage_key, filename");
|
|
57
|
+
}
|
|
58
|
+
return client.appCompleteFileUpload(body.app_id, {
|
|
59
|
+
file_id: p.file_id,
|
|
60
|
+
file_storage_key: p.file_storage_key,
|
|
61
|
+
filename: p.filename,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
31
64
|
default: {
|
|
32
65
|
// Unreachable — SUPPORTED_OPS gates above.
|
|
33
66
|
throw new Error(`Unhandled RPC op: ${body.op}`);
|
|
@@ -59,24 +59,59 @@ export function buildWrapperPage(args) {
|
|
|
59
59
|
const APP_ID = ${JSON.stringify(app_id)};
|
|
60
60
|
const iframe = document.getElementById("app");
|
|
61
61
|
|
|
62
|
+
async function rpc(op, payload) {
|
|
63
|
+
const res = await fetch("/_rpc", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
|
|
67
|
+
});
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let detail = text;
|
|
71
|
+
try { detail = JSON.parse(text).message ?? text; } catch (_) {}
|
|
72
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
73
|
+
}
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The iframe SDK sends one "upload" op carrying a File. A File can't
|
|
78
|
+
// cross the JSON /_rpc hop, so the upload runs here in the browser:
|
|
79
|
+
// mint a presigned URL, PUT the bytes to storage, then finalize.
|
|
80
|
+
async function handleUpload(payload) {
|
|
81
|
+
const file = payload && payload.file;
|
|
82
|
+
if (!(file instanceof File)) {
|
|
83
|
+
throw new Error("upload payload must include a File");
|
|
84
|
+
}
|
|
85
|
+
const init = await rpc("upload_url", {
|
|
86
|
+
filename: file.name,
|
|
87
|
+
mime_type: file.type,
|
|
88
|
+
file_size: file.size,
|
|
89
|
+
});
|
|
90
|
+
const putRes = await fetch(init.upload_url, {
|
|
91
|
+
method: "PUT",
|
|
92
|
+
body: file,
|
|
93
|
+
headers: { "Content-Type": file.type },
|
|
94
|
+
});
|
|
95
|
+
if (!putRes.ok) {
|
|
96
|
+
throw new Error("Storage upload failed (" + putRes.status + ")");
|
|
97
|
+
}
|
|
98
|
+
const completed = await rpc("upload_complete", {
|
|
99
|
+
file_id: init.file_id,
|
|
100
|
+
file_storage_key: init.file_storage_key,
|
|
101
|
+
filename: file.name,
|
|
102
|
+
});
|
|
103
|
+
return completed.file;
|
|
104
|
+
}
|
|
105
|
+
|
|
62
106
|
window.addEventListener("message", async function (event) {
|
|
63
107
|
if (event.source !== iframe.contentWindow) return;
|
|
64
108
|
const msg = event.data;
|
|
65
109
|
if (!msg || typeof msg.id !== "number" || typeof msg.op !== "string") return;
|
|
66
110
|
const startedAt = performance.now();
|
|
67
111
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
body: JSON.stringify({ app_id: APP_ID, op: msg.op, payload: msg.payload }),
|
|
72
|
-
});
|
|
73
|
-
const text = await res.text();
|
|
74
|
-
if (!res.ok) {
|
|
75
|
-
let detail = text;
|
|
76
|
-
try { detail = JSON.parse(text).message ?? text; } catch (_) {}
|
|
77
|
-
throw new Error(detail || ("HTTP " + res.status));
|
|
78
|
-
}
|
|
79
|
-
const data = JSON.parse(text);
|
|
112
|
+
const data = msg.op === "upload"
|
|
113
|
+
? await handleUpload(msg.payload)
|
|
114
|
+
: await rpc(msg.op, msg.payload);
|
|
80
115
|
const ms = Math.round(performance.now() - startedAt);
|
|
81
116
|
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
82
117
|
iframe.contentWindow.postMessage({ id: msg.id, type: "result", data: data }, "*");
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
|
-
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
*
|
|
13
|
+
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
+
* `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
|
|
15
|
+
* target @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
|
+
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
17
|
+
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
15
18
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
16
19
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
17
20
|
* Don't switch to `base: "./"` without updating `rewriteAssetPaths`.
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
|
-
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
*
|
|
13
|
+
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
+
* `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
|
|
15
|
+
* target @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
|
+
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
17
|
+
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
15
18
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
16
19
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
17
20
|
* Don't switch to `base: "./"` without updating `rewriteAssetPaths`.
|
|
@@ -47,8 +50,8 @@ export function buildStarterTemplate(args) {
|
|
|
47
50
|
test: "vitest run",
|
|
48
51
|
},
|
|
49
52
|
dependencies: {
|
|
50
|
-
"@lotics/app-sdk": "^0.
|
|
51
|
-
"@lotics/ui": "^
|
|
53
|
+
"@lotics/app-sdk": "^0.6.0",
|
|
54
|
+
"@lotics/ui": "^1.3.0",
|
|
52
55
|
"@react-native-picker/picker": "^2.7.0",
|
|
53
56
|
"expo-image": "~3.0.9",
|
|
54
57
|
"lucide-react": "^0.562.0",
|
|
@@ -135,6 +138,12 @@ export default defineConfig({
|
|
|
135
138
|
{ find: "react-native", replacement: "react-native-web" },
|
|
136
139
|
],
|
|
137
140
|
extensions: [".web.tsx", ".web.ts", ".tsx", ".ts", ".jsx", ".js"],
|
|
141
|
+
// @lotics/ui ships source and is consumed across many subpath entries
|
|
142
|
+
// (./card, ./metric, ./use_screen_size, …). Without dedupe, Vite can
|
|
143
|
+
// pre-bundle a subpath into its own chunk with a second React copy — a
|
|
144
|
+
// hook called from there hits a null dispatcher ("Invalid hook call").
|
|
145
|
+
// Pin React (and RN-Web) to a single instance shared by every chunk.
|
|
146
|
+
dedupe: ["react", "react-dom", "react-native-web"],
|
|
138
147
|
},
|
|
139
148
|
build: {
|
|
140
149
|
outDir: "dist",
|
|
@@ -186,10 +195,18 @@ export default defineConfig({
|
|
|
186
195
|
{
|
|
187
196
|
path: "src/main.tsx",
|
|
188
197
|
content: `import "@lotics/ui/index.css";
|
|
198
|
+
import "@lotics/ui/fonts.css";
|
|
199
|
+
import { PortalHost } from "@lotics/ui/portal";
|
|
189
200
|
import { mount } from "@lotics/app-sdk";
|
|
190
201
|
import App from "./App";
|
|
191
202
|
|
|
192
|
-
|
|
203
|
+
// PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
|
|
204
|
+
// Dialog). Without it, Portal renders nothing — keep it wrapping the app.
|
|
205
|
+
mount(
|
|
206
|
+
<PortalHost>
|
|
207
|
+
<App />
|
|
208
|
+
</PortalHost>,
|
|
209
|
+
);
|
|
193
210
|
`,
|
|
194
211
|
},
|
|
195
212
|
{
|