@juggernautlabs/views 0.0.1
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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/cli.js +320 -0
- package/dist/index.js +308 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Juggernaut Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
File without changes
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/cli.js
|
|
4
|
+
import { cac } from "cac";
|
|
5
|
+
|
|
6
|
+
// src/bundler.js
|
|
7
|
+
import { build } from "vite";
|
|
8
|
+
import react from "@vitejs/plugin-react";
|
|
9
|
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
|
10
|
+
import path3 from "path";
|
|
11
|
+
|
|
12
|
+
// src/uploader.js
|
|
13
|
+
import fs2 from "fs";
|
|
14
|
+
import path2 from "path";
|
|
15
|
+
import JSON52 from "json5";
|
|
16
|
+
|
|
17
|
+
// src/api.js
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import JSON5 from "json5";
|
|
21
|
+
import dotenv from "dotenv";
|
|
22
|
+
var BASE_ROUTE = "https://core.juggernautlabs.ai";
|
|
23
|
+
var buildRoute = (route) => {
|
|
24
|
+
return `${BASE_ROUTE}${route}`;
|
|
25
|
+
};
|
|
26
|
+
var isEnvLoaded = false;
|
|
27
|
+
var loadEnvVariable = (key, defaultValue = "") => {
|
|
28
|
+
if (!isEnvLoaded) {
|
|
29
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
30
|
+
if (fs.existsSync(envPath)) {
|
|
31
|
+
dotenv.config({ path: envPath });
|
|
32
|
+
}
|
|
33
|
+
isEnvLoaded = true;
|
|
34
|
+
}
|
|
35
|
+
const value = import.meta.env?.[key] || process.env?.[key];
|
|
36
|
+
return value || defaultValue;
|
|
37
|
+
};
|
|
38
|
+
var executeRequest = async (route, method = "POST", body = null, headers = {}) => {
|
|
39
|
+
const url = buildRoute(route);
|
|
40
|
+
const options = {
|
|
41
|
+
method,
|
|
42
|
+
headers: {
|
|
43
|
+
...headers,
|
|
44
|
+
authorization: `Bearer ${await loadEnvVariable("JUGGERNAUT_API_KEY")}`,
|
|
45
|
+
"client-key": `${await loadEnvVariable("JUGGERNAUT_CLIENT_ID")}`
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (body) {
|
|
49
|
+
if (body instanceof FormData) {
|
|
50
|
+
options.body = body;
|
|
51
|
+
} else {
|
|
52
|
+
options.headers["Content-Type"] = "application/json";
|
|
53
|
+
options.body = JSON.stringify(body);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(url, options);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
throw new Error(`Request failed with status ${response.status}: ${errorText}`);
|
|
60
|
+
}
|
|
61
|
+
return response.json();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/uploader.js
|
|
65
|
+
import { promises } from "fs";
|
|
66
|
+
var ROUTES = {
|
|
67
|
+
ingest: "/api/resource/ui/template/ingest",
|
|
68
|
+
uploadSolution: "/api/resource/ui/template/upload/solution",
|
|
69
|
+
create: "/api/resource/ui/template"
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
class TemplateUploader {
|
|
73
|
+
static async parseMetadataFile(filePath) {
|
|
74
|
+
if (!fs2.existsSync(filePath)) {
|
|
75
|
+
throw new Error(`Metadata file not found at path: ${filePath}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const fileContent = await fs2.readFileSync(filePath, "utf-8");
|
|
79
|
+
const metadata = JSON52.parse(fileContent) || {};
|
|
80
|
+
return {
|
|
81
|
+
name: metadata.name || "Untitled Template",
|
|
82
|
+
description: metadata.description || "",
|
|
83
|
+
tags: metadata.tags || [],
|
|
84
|
+
target: {
|
|
85
|
+
role: metadata.target?.role,
|
|
86
|
+
resource: metadata.target?.resource,
|
|
87
|
+
resourceType: metadata.target?.resourceType
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
} catch (e) {
|
|
91
|
+
throw new Error("Invalid JSON in metadata file");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
static async findConfigFile() {
|
|
95
|
+
const currentDir = process.cwd();
|
|
96
|
+
const configFilePath = path2.join(currentDir, ".juggernauttemplate");
|
|
97
|
+
if (fs2.existsSync(configFilePath)) {
|
|
98
|
+
return this.parseMetadataFile(configFilePath);
|
|
99
|
+
} else {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
static async upload(file, metadataPath = null) {
|
|
104
|
+
let templateDetails = {};
|
|
105
|
+
if (metadataPath) {
|
|
106
|
+
templateDetails = await this.parseMetadataFile(metadataPath);
|
|
107
|
+
}
|
|
108
|
+
return this.uploadTemplate(templateDetails, file);
|
|
109
|
+
}
|
|
110
|
+
static async getUploadURL() {
|
|
111
|
+
const { data } = await executeRequest(ROUTES.uploadSolution, "GET");
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
static createTemplate(templateDetails) {
|
|
115
|
+
return executeRequest(ROUTES.ingest, "POST", templateDetails);
|
|
116
|
+
}
|
|
117
|
+
static async uploadTemplate(viewTemplateDetails, templatePath) {
|
|
118
|
+
const { name, url } = await this.getUploadURL();
|
|
119
|
+
const fileContent = await promises.readFile(templatePath);
|
|
120
|
+
const uploadSolutionResponse = await fetch(url, {
|
|
121
|
+
method: "PUT",
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/html",
|
|
124
|
+
"x-ms-blob-type": "BlockBlob"
|
|
125
|
+
},
|
|
126
|
+
body: fileContent
|
|
127
|
+
});
|
|
128
|
+
if (uploadSolutionResponse.ok) {
|
|
129
|
+
console.log("Upload successful!");
|
|
130
|
+
} else {
|
|
131
|
+
const errorText = await uploadSolutionResponse.text();
|
|
132
|
+
throw new Error(`Failed to upload: ${uploadSolutionResponse.status} - ${errorText}`);
|
|
133
|
+
}
|
|
134
|
+
return this.createTemplate({
|
|
135
|
+
...viewTemplateDetails,
|
|
136
|
+
solution: name
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/bundler.js
|
|
142
|
+
import fs3 from "fs";
|
|
143
|
+
async function bundle({ entry, outDir, root, template = {} }) {
|
|
144
|
+
const isScript = /\.(js|ts|jsx|tsx)$/.test(entry);
|
|
145
|
+
let viteConfig = {
|
|
146
|
+
root,
|
|
147
|
+
configFile: false,
|
|
148
|
+
plugins: [
|
|
149
|
+
react(),
|
|
150
|
+
viteSingleFile({
|
|
151
|
+
useRecommendedBuildConfig: true,
|
|
152
|
+
removeViteModuleLoader: true
|
|
153
|
+
}),
|
|
154
|
+
isScript && virtualHtmlPlugin(entry)
|
|
155
|
+
],
|
|
156
|
+
build: {
|
|
157
|
+
outDir,
|
|
158
|
+
emptyOutDir: true,
|
|
159
|
+
minify: true
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
await build(viteConfig);
|
|
163
|
+
const bundlePath = path3.join(outDir, "index.html");
|
|
164
|
+
return TemplateUploader.uploadTemplate(template, bundlePath);
|
|
165
|
+
}
|
|
166
|
+
function virtualHtmlPlugin(entryScriptPath) {
|
|
167
|
+
const virtualHtmlId = "index.html";
|
|
168
|
+
const resolvedHtmlId = "/index.html";
|
|
169
|
+
const virtualEntryId = "virtual-auto-mount.tsx";
|
|
170
|
+
const resolvedEntryId = "\x00virtual-auto-mount.tsx";
|
|
171
|
+
return {
|
|
172
|
+
name: "auto-html-entry",
|
|
173
|
+
resolveId(id) {
|
|
174
|
+
if (id === virtualHtmlId)
|
|
175
|
+
return resolvedHtmlId;
|
|
176
|
+
if (id === virtualEntryId)
|
|
177
|
+
return resolvedEntryId;
|
|
178
|
+
},
|
|
179
|
+
load(id) {
|
|
180
|
+
if (id === resolvedHtmlId) {
|
|
181
|
+
const code = fs3.readFileSync(entryScriptPath, "utf-8");
|
|
182
|
+
const hasRender = code.includes("ReactDOM") || code.includes("createRoot");
|
|
183
|
+
const entrySrc = hasRender ? `/${path3.relative(process.cwd(), entryScriptPath)}` : virtualEntryId;
|
|
184
|
+
return `
|
|
185
|
+
<!DOCTYPE html>
|
|
186
|
+
<html>
|
|
187
|
+
<head>
|
|
188
|
+
<meta charset="utf-8">
|
|
189
|
+
<title>React App</title>
|
|
190
|
+
<style>html,body,#root{height:100%;margin:0;}</style>
|
|
191
|
+
</head>
|
|
192
|
+
<body>
|
|
193
|
+
<div id="root"></div>
|
|
194
|
+
<script type="module" src="${entrySrc}"></script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
if (id === resolvedEntryId) {
|
|
200
|
+
const importPath = entryScriptPath.split(path3.sep).join("/");
|
|
201
|
+
return `
|
|
202
|
+
import React from 'react';
|
|
203
|
+
import ReactDOM from 'react-dom/client';
|
|
204
|
+
import UserApp from '${importPath}';
|
|
205
|
+
|
|
206
|
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
207
|
+
root.render(
|
|
208
|
+
React.createElement(React.StrictMode, null,
|
|
209
|
+
React.createElement(UserApp)
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
configureServer(server) {
|
|
216
|
+
server.middlewares.use((req, res, next) => {
|
|
217
|
+
if (req.url === "/")
|
|
218
|
+
req.url = "/index.html";
|
|
219
|
+
next();
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
config() {
|
|
223
|
+
return {
|
|
224
|
+
build: {
|
|
225
|
+
rollupOptions: { input: resolvedHtmlId }
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function getViteConfig({ entry, root, outDir, isDev = false }) {
|
|
232
|
+
const isScript = /\.(js|ts|jsx|tsx)$/.test(entry);
|
|
233
|
+
const plugins = [
|
|
234
|
+
react(),
|
|
235
|
+
!isDev && viteSingleFile({
|
|
236
|
+
useRecommendedBuildConfig: true,
|
|
237
|
+
removeViteModuleLoader: true
|
|
238
|
+
}),
|
|
239
|
+
isScript && virtualHtmlPlugin(entry)
|
|
240
|
+
].filter(Boolean);
|
|
241
|
+
return {
|
|
242
|
+
root,
|
|
243
|
+
configFile: false,
|
|
244
|
+
plugins,
|
|
245
|
+
server: {
|
|
246
|
+
fs: {
|
|
247
|
+
allow: [root, process.cwd()]
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
build: {
|
|
251
|
+
outDir,
|
|
252
|
+
emptyOutDir: true,
|
|
253
|
+
minify: !isDev,
|
|
254
|
+
assetsInlineLimit: 1e8,
|
|
255
|
+
rollupOptions: {
|
|
256
|
+
output: {
|
|
257
|
+
inlineDynamicImports: !isDev
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// bin/cli.js
|
|
265
|
+
import path4 from "path";
|
|
266
|
+
import { createServer } from "vite";
|
|
267
|
+
var cli = cac("templatebundler");
|
|
268
|
+
cli.command("<entry>", "Bundle the React app into a single file").option("-o, --output <path>", "Output directory", { default: "dist" }).option("-c, --config <path>", "Input config file (.juggernauttemplate) or a .json file").action(async (entry, options) => {
|
|
269
|
+
try {
|
|
270
|
+
const rootDir = process.cwd();
|
|
271
|
+
const absoluteEntry = path4.resolve(rootDir, entry);
|
|
272
|
+
const absoluteOutput = path4.resolve(rootDir, options.output);
|
|
273
|
+
const configPath = options.config ? path4.resolve(rootDir, options.config) : null;
|
|
274
|
+
console.log(`\uD83D\uDCE6 Bundling ${entry}...`);
|
|
275
|
+
const template = configPath ? await TemplateUploader.parseMetadataFile(configPath) : await TemplateUploader.findConfigFile();
|
|
276
|
+
if (template)
|
|
277
|
+
console.log(`\uD83D\uDCDD Using template config: ${configPath || ".juggernauttemplate"}`);
|
|
278
|
+
else
|
|
279
|
+
throw new Error("No template configuration provided and no .juggernauttemplate file found.");
|
|
280
|
+
await bundle({
|
|
281
|
+
entry: absoluteEntry,
|
|
282
|
+
outDir: absoluteOutput,
|
|
283
|
+
root: rootDir,
|
|
284
|
+
template
|
|
285
|
+
});
|
|
286
|
+
console.log(`✅ Done! Output saved to ${options.output}`);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error("❌ Build failed:", e);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
cli.command("dev <entry>", "Start a dev server with HMR").option("-p, --port <port>", "Port to listen on", { default: 5173 }).action(async (entry, options) => {
|
|
293
|
+
try {
|
|
294
|
+
const rootDir = process.cwd();
|
|
295
|
+
const absoluteEntry = path4.resolve(rootDir, entry);
|
|
296
|
+
console.log(`⚡ Starting Dev Server for ${entry}...`);
|
|
297
|
+
const viteConfig = getViteConfig({
|
|
298
|
+
entry: absoluteEntry,
|
|
299
|
+
root: rootDir,
|
|
300
|
+
outDir: "dist",
|
|
301
|
+
isDev: true
|
|
302
|
+
});
|
|
303
|
+
const server = await createServer({
|
|
304
|
+
...viteConfig,
|
|
305
|
+
server: {
|
|
306
|
+
port: options.port,
|
|
307
|
+
host: true
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
await server.listen();
|
|
311
|
+
server.printUrls();
|
|
312
|
+
console.log(`
|
|
313
|
+
\uD83E\uDDEA Demo Data: ensure you have a .demo.json in your root to mock server data.`);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error("❌ Dev server failed:", e);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
cli.help();
|
|
320
|
+
cli.parse();
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// src/hooks.jsx
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/context.js
|
|
5
|
+
var STORE_KEY = Symbol("ContextStore");
|
|
6
|
+
|
|
7
|
+
class ContextManager {
|
|
8
|
+
static [STORE_KEY] = {
|
|
9
|
+
job: null,
|
|
10
|
+
record: null,
|
|
11
|
+
data: null,
|
|
12
|
+
plugin: null,
|
|
13
|
+
process: null,
|
|
14
|
+
actions: [],
|
|
15
|
+
system: null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/hooks.jsx
|
|
20
|
+
var IS_DEV_MODE = import.meta.env ? import.meta.env.DEV : process?.env.NODE_ENV === "development";
|
|
21
|
+
var DEMO_FILE_PATH = "/.demo.json";
|
|
22
|
+
var subscribers = new Set;
|
|
23
|
+
var isListening = false;
|
|
24
|
+
var handleContextWindowEvents = (event) => {
|
|
25
|
+
const { type, payload } = event.data || {};
|
|
26
|
+
if (!type || !payload)
|
|
27
|
+
return;
|
|
28
|
+
const { resourceType, resource } = payload;
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "juggernaut:context:request":
|
|
31
|
+
if (ContextManager[STORE_KEY] && ContextManager[STORE_KEY].hasOwnProperty(resourceType)) {
|
|
32
|
+
const currentResource = ContextManager[STORE_KEY][resourceType];
|
|
33
|
+
window.parent.postMessage({
|
|
34
|
+
type: "juggernaut:context:response",
|
|
35
|
+
payload: { resourceType, resource: currentResource }
|
|
36
|
+
}, "*");
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case "juggernaut:context:update":
|
|
40
|
+
if (ContextManager[STORE_KEY]) {
|
|
41
|
+
ContextManager[STORE_KEY][resourceType] = resource;
|
|
42
|
+
subscribers.forEach((callback) => callback(resourceType, resource));
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var listenForJuggernautPlatformEvents = () => {
|
|
50
|
+
if (typeof window === "undefined")
|
|
51
|
+
return;
|
|
52
|
+
if (!window.parent && !IS_DEV_MODE)
|
|
53
|
+
return;
|
|
54
|
+
if (!isListening) {
|
|
55
|
+
window.addEventListener("message", handleContextWindowEvents);
|
|
56
|
+
isListening = true;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var useJuggernautResource = (resourceType) => {
|
|
60
|
+
const [resource, setResource] = useState(() => {
|
|
61
|
+
if (ContextManager[STORE_KEY] && ContextManager[STORE_KEY][resourceType]) {
|
|
62
|
+
return ContextManager[STORE_KEY][resourceType];
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
});
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let isMounted = true;
|
|
68
|
+
const tryLoadDemoData = async () => {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(DEMO_FILE_PATH);
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw new Error("No demo file");
|
|
73
|
+
const demoData = await response.json();
|
|
74
|
+
if (demoData && demoData[resourceType] && isMounted) {
|
|
75
|
+
console.log(`[Juggernaut SDK] Loaded DEMO data for: ${resourceType}`);
|
|
76
|
+
if (ContextManager[STORE_KEY]) {
|
|
77
|
+
ContextManager[STORE_KEY][resourceType] = demoData[resourceType];
|
|
78
|
+
}
|
|
79
|
+
setResource(demoData[resourceType]);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.warn("[Juggernaut SDK] Dev mode active, but no valid .demo.json found.");
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
const initialize = async () => {
|
|
88
|
+
if (IS_DEV_MODE) {
|
|
89
|
+
const loadedFromDemo = await tryLoadDemoData();
|
|
90
|
+
if (loadedFromDemo)
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
listenForJuggernautPlatformEvents();
|
|
94
|
+
const handleUpdate = (type, data) => {
|
|
95
|
+
if (isMounted && type === resourceType) {
|
|
96
|
+
setResource(data);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
subscribers.add(handleUpdate);
|
|
100
|
+
if (!resource && window.parent) {
|
|
101
|
+
window.parent.postMessage({
|
|
102
|
+
type: "juggernaut:context:request",
|
|
103
|
+
payload: { resourceType }
|
|
104
|
+
}, "*");
|
|
105
|
+
}
|
|
106
|
+
return () => {
|
|
107
|
+
subscribers.delete(handleUpdate);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const cleanupFnPromise = initialize();
|
|
111
|
+
return () => {
|
|
112
|
+
isMounted = false;
|
|
113
|
+
cleanupFnPromise.then((cleanup) => cleanup && cleanup());
|
|
114
|
+
};
|
|
115
|
+
}, [resourceType]);
|
|
116
|
+
return resource;
|
|
117
|
+
};
|
|
118
|
+
var sendJuggernautActionEvent = (actionType, payload) => {
|
|
119
|
+
if (typeof window === "undefined")
|
|
120
|
+
return;
|
|
121
|
+
if (IS_DEV_MODE) {
|
|
122
|
+
console.log(`[Juggernaut Action] ${actionType}`, payload);
|
|
123
|
+
}
|
|
124
|
+
if (!window.parent)
|
|
125
|
+
return;
|
|
126
|
+
window.parent.postMessage({
|
|
127
|
+
type: actionType,
|
|
128
|
+
payload
|
|
129
|
+
}, "*");
|
|
130
|
+
};
|
|
131
|
+
var useJuggernautJob = () => useJuggernautResource("job");
|
|
132
|
+
var useJuggernautRecord = () => useJuggernautResource("record");
|
|
133
|
+
var useJuggernautData = () => useJuggernautResource("data");
|
|
134
|
+
var useJuggernautPlugin = () => useJuggernautResource("plugin");
|
|
135
|
+
var useJuggernautProcess = () => useJuggernautResource("process");
|
|
136
|
+
var useJuggernautActions = () => useJuggernautResource("actions");
|
|
137
|
+
var useJuggernautSystem = () => useJuggernautResource("system");
|
|
138
|
+
var FILE_EVENTS = {
|
|
139
|
+
GET_FILE: "juggernaut:file:get",
|
|
140
|
+
GET_JOB_FILE: "juggernaut:job:file:get",
|
|
141
|
+
UPLOAD_FILE: "juggernaut:file:upload"
|
|
142
|
+
};
|
|
143
|
+
var useJuggernautFileSystem = () => {
|
|
144
|
+
const requestFile = useCallback((actionType, payload) => {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
if (IS_DEV_MODE) {
|
|
147
|
+
console.log(`[Dev Mode] Fetching file for ${actionType}`, payload);
|
|
148
|
+
const fileName = payload.fileId || payload.attachmentName || "demo.png";
|
|
149
|
+
fetch(`/demo-files/${fileName}`).then((res) => {
|
|
150
|
+
if (!res.ok)
|
|
151
|
+
throw new Error("Demo file not found");
|
|
152
|
+
return res.blob();
|
|
153
|
+
}).then((blob) => {
|
|
154
|
+
const file = new File([blob], fileName, { type: blob.type });
|
|
155
|
+
resolve(file);
|
|
156
|
+
}).catch((err) => {
|
|
157
|
+
console.warn("[Dev Mode] Could not load demo file:", err);
|
|
158
|
+
reject(err);
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (typeof window === "undefined" || !window.parent) {
|
|
163
|
+
return reject(new Error("No parent window found"));
|
|
164
|
+
}
|
|
165
|
+
const requestId = crypto.randomUUID();
|
|
166
|
+
const handleResponse = (event) => {
|
|
167
|
+
const { requestId: resId, payload: responsePayload, error } = event.data || {};
|
|
168
|
+
if (resId === requestId) {
|
|
169
|
+
window.removeEventListener("message", handleResponse);
|
|
170
|
+
if (error) {
|
|
171
|
+
reject(new Error(error));
|
|
172
|
+
} else {
|
|
173
|
+
resolve(responsePayload);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
window.addEventListener("message", handleResponse);
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
window.removeEventListener("message", handleResponse);
|
|
180
|
+
reject(new Error("File request timed out"));
|
|
181
|
+
}, 1e4);
|
|
182
|
+
window.parent.postMessage({
|
|
183
|
+
type: actionType,
|
|
184
|
+
requestId,
|
|
185
|
+
payload
|
|
186
|
+
}, "*");
|
|
187
|
+
});
|
|
188
|
+
}, []);
|
|
189
|
+
const getFile = useCallback((fileId) => {
|
|
190
|
+
if (!fileId)
|
|
191
|
+
return Promise.reject("No fileId provided");
|
|
192
|
+
return requestFile(FILE_EVENTS.GET_FILE, { fileId });
|
|
193
|
+
}, [requestFile]);
|
|
194
|
+
const getJobFile = useCallback((link, jobId = null) => {
|
|
195
|
+
if (!jobId || !link)
|
|
196
|
+
return Promise.reject("Missing jobId or link");
|
|
197
|
+
return requestFile(FILE_EVENTS.GET_JOB_FILE, { link, jobId });
|
|
198
|
+
}, [requestFile]);
|
|
199
|
+
return {
|
|
200
|
+
getFile,
|
|
201
|
+
getJobFile
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
// src/actions.jsx
|
|
205
|
+
import { useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
206
|
+
var ActionEvents = {
|
|
207
|
+
START_JOB: "start:job",
|
|
208
|
+
SCHEDULE_JOB: "schedule:job",
|
|
209
|
+
OPEN_RESOURCE: "open:resource",
|
|
210
|
+
START_PLUGIN: "start:plugin",
|
|
211
|
+
DATABASE_OPERATION: "database:operation",
|
|
212
|
+
TRIGGER_ACTION: "trigger:action",
|
|
213
|
+
CONTEXT_REQUEST: "juggernaut:context:request",
|
|
214
|
+
CONTEXT_RESPONSE: "juggernaut:context:response",
|
|
215
|
+
CONTEXT_UPDATE: "juggernaut:context:update"
|
|
216
|
+
};
|
|
217
|
+
var RESPONSE_TIMEOUT_MS = 5000;
|
|
218
|
+
var useBridge = () => {
|
|
219
|
+
const pendingRequests = useRef2(new Map);
|
|
220
|
+
useEffect2(() => {
|
|
221
|
+
const handleMessage = (event) => {
|
|
222
|
+
const { requestId, payload, error } = event.data || {};
|
|
223
|
+
if (requestId && pendingRequests.current.has(requestId)) {
|
|
224
|
+
const { resolve, reject, timer } = pendingRequests.current.get(requestId);
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
pendingRequests.current.delete(requestId);
|
|
227
|
+
if (error) {
|
|
228
|
+
reject(new Error(error));
|
|
229
|
+
} else {
|
|
230
|
+
resolve(payload);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
window.addEventListener("message", handleMessage);
|
|
235
|
+
return () => {
|
|
236
|
+
window.removeEventListener("message", handleMessage);
|
|
237
|
+
pendingRequests.current.forEach(({ reject, timer }) => {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
reject(new Error("Component unmounted"));
|
|
240
|
+
});
|
|
241
|
+
pendingRequests.current.clear();
|
|
242
|
+
};
|
|
243
|
+
}, []);
|
|
244
|
+
const sendEvent = useCallback2((actionType, payload = {}) => {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
if (typeof window === "undefined" || !window?.parent) {
|
|
247
|
+
return reject(new Error("Parent window not found"));
|
|
248
|
+
}
|
|
249
|
+
const requestId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
|
|
250
|
+
const timer = setTimeout(() => {
|
|
251
|
+
if (pendingRequests.current.has(requestId)) {
|
|
252
|
+
pendingRequests.current.delete(requestId);
|
|
253
|
+
reject(new Error(`Request timed out after ${RESPONSE_TIMEOUT_MS}ms`));
|
|
254
|
+
}
|
|
255
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
256
|
+
pendingRequests.current.set(requestId, { resolve, reject, timer });
|
|
257
|
+
window.parent.postMessage({
|
|
258
|
+
type: actionType,
|
|
259
|
+
requestId,
|
|
260
|
+
payload
|
|
261
|
+
}, "*");
|
|
262
|
+
});
|
|
263
|
+
}, []);
|
|
264
|
+
const startJob = useCallback2((processId, inputs, metadata = {}) => {
|
|
265
|
+
return sendEvent(ActionEvents.START_JOB, { processId, inputs, metadata });
|
|
266
|
+
}, [sendEvent]);
|
|
267
|
+
const scheduleJob = useCallback2((processId, inputs, schedule, metadata = {}) => {
|
|
268
|
+
return sendEvent(ActionEvents.SCHEDULE_JOB, { processId, inputs, schedule, metadata });
|
|
269
|
+
}, [sendEvent]);
|
|
270
|
+
const openResource = useCallback2((type, id, extra = {}) => {
|
|
271
|
+
return sendEvent(ActionEvents.OPEN_RESOURCE, { resourceType: type, resourceId: id, ...extra });
|
|
272
|
+
}, [sendEvent]);
|
|
273
|
+
const triggerAction = useCallback2((payload) => sendEvent(ActionEvents.TRIGGER_ACTION, payload), [sendEvent]);
|
|
274
|
+
const launchProcess = (id, options = {}) => openResource("process", id, options);
|
|
275
|
+
const launchJob = (id, options = {}) => openResource("job", id, options);
|
|
276
|
+
const launchPlugin = (id, options = {}) => openResource("plugin", id, options);
|
|
277
|
+
const launchDatabase = (id, options = {}) => openResource("database", id, options);
|
|
278
|
+
const launchRecord = (dbId, recordId, options = {}) => openResource("database:record", recordId, { database: dbId, ...options });
|
|
279
|
+
const startPlugin = useCallback2((payload) => sendEvent(ActionEvents.START_PLUGIN, payload), [sendEvent]);
|
|
280
|
+
const databaseOperation = useCallback2((payload) => sendEvent(ActionEvents.DATABASE_OPERATION, payload), [sendEvent]);
|
|
281
|
+
return {
|
|
282
|
+
startJob,
|
|
283
|
+
scheduleJob,
|
|
284
|
+
launchProcess,
|
|
285
|
+
launchJob,
|
|
286
|
+
launchPlugin,
|
|
287
|
+
launchDatabase,
|
|
288
|
+
launchRecord,
|
|
289
|
+
startPlugin,
|
|
290
|
+
databaseOperation,
|
|
291
|
+
triggerAction,
|
|
292
|
+
sendEvent
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
export {
|
|
296
|
+
useJuggernautSystem,
|
|
297
|
+
useJuggernautRecord,
|
|
298
|
+
useJuggernautProcess,
|
|
299
|
+
useJuggernautPlugin,
|
|
300
|
+
useJuggernautJob,
|
|
301
|
+
useJuggernautFileSystem,
|
|
302
|
+
useJuggernautData,
|
|
303
|
+
useJuggernautActions,
|
|
304
|
+
useBridge,
|
|
305
|
+
sendJuggernautActionEvent,
|
|
306
|
+
listenForJuggernautPlatformEvents,
|
|
307
|
+
ActionEvents
|
|
308
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juggernautlabs/views",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A way to build Juggernaut view templates using React.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"juggernauttemplates": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test --preload ./bun-setup.js",
|
|
13
|
+
"build": "bun run build:cli && bun run build:lib",
|
|
14
|
+
"build:cli": "bun build ./bin/cli.js --outfile dist/cli.js --target node --packages external",
|
|
15
|
+
"build:lib": "bun build ./src/index.js --outfile dist/index.js --target browser --packages external --format esm"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"platform",
|
|
22
|
+
"cloud",
|
|
23
|
+
"sdk",
|
|
24
|
+
"api",
|
|
25
|
+
"plugin",
|
|
26
|
+
"juggernaut",
|
|
27
|
+
"juggernautlabs",
|
|
28
|
+
"plugins",
|
|
29
|
+
"llm integration",
|
|
30
|
+
"mcp alternative",
|
|
31
|
+
"workflow automation",
|
|
32
|
+
"developer tools"
|
|
33
|
+
],
|
|
34
|
+
"author": "Juggernaut Labs",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
38
|
+
"bun": "^1.3.4",
|
|
39
|
+
"cac": "^6.7.0",
|
|
40
|
+
"dotenv": "^17.2.3",
|
|
41
|
+
"json5": "^2.2.3",
|
|
42
|
+
"react": "^19.2.1",
|
|
43
|
+
"vite": "^6.0.0",
|
|
44
|
+
"vite-plugin-singlefile": "^2.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@happy-dom/global-registrator": "^20.0.11",
|
|
48
|
+
"@testing-library/react": "^16.3.0",
|
|
49
|
+
"@types/jest": "^30.0.0",
|
|
50
|
+
"happy-dom": "^20.0.11",
|
|
51
|
+
"jest": "^30.2.0",
|
|
52
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
53
|
+
"ts-jest": "^29.4.6"
|
|
54
|
+
}
|
|
55
|
+
}
|