@pulse-editor/cli 0.1.3 → 0.1.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/dist/components/commands/code.js +27 -3
- package/dist/lib/server/express.js +9 -0
- package/dist/lib/server/preview/frontend/index.js +63 -6
- package/dist/lib/webpack/configs/mf-client.js +7 -7
- package/dist/lib/webpack/configs/mf-server.js +6 -254
- package/dist/lib/webpack/configs/preview.d.ts +1 -0
- package/dist/lib/webpack/configs/preview.js +131 -10
- package/dist/lib/webpack/configs/utils.d.ts +11 -0
- package/dist/lib/webpack/configs/utils.js +221 -2
- package/dist/lib/webpack/webpack-config.js +3 -3
- package/package.json +1 -1
- package/dist/lib/server/utils.d.ts +0 -1
- package/dist/lib/server/utils.js +0 -17
- package/dist/lib/webpack/dist/pregistered-actions.d.ts +0 -2
- package/dist/lib/webpack/dist/pulse.config.d.ts +0 -7
- package/dist/lib/webpack/dist/src/lib/agents/code-modifier-agent.d.ts +0 -2
- package/dist/lib/webpack/dist/src/lib/agents/vibe-coding-agent.d.ts +0 -3
- package/dist/lib/webpack/dist/src/lib/mcp/utils.d.ts +0 -3
- package/dist/lib/webpack/dist/src/lib/streaming/message-stream-controller.d.ts +0 -10
- package/dist/lib/webpack/dist/src/lib/types.d.ts +0 -58
- package/dist/lib/webpack/dist/src/server-function/generate-code/v1/generate.d.ts +0 -5
- package/dist/lib/webpack/dist/src/server-function/generate-code/v2/generate.d.ts +0 -1
- package/dist/lib/webpack/tsconfig.server.json +0 -19
- package/dist/lib/webpack/webpack.config.d.ts +0 -2
- package/dist/lib/webpack/webpack.config.js +0 -527
|
@@ -5,7 +5,7 @@ import Spinner from "ink-spinner";
|
|
|
5
5
|
import TextInput from "ink-text-input";
|
|
6
6
|
import JSZip from "jszip";
|
|
7
7
|
import path from "path";
|
|
8
|
-
import { useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { getBackendUrl } from "../../lib/backend-url.js";
|
|
10
10
|
import { checkToken, getToken } from "../../lib/token.js";
|
|
11
11
|
function readContinueData(cwd) {
|
|
@@ -77,6 +77,8 @@ export default function Code({ cli }) {
|
|
|
77
77
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
78
78
|
const [isGenerated, setIsGenerated] = useState(false);
|
|
79
79
|
const [error, setError] = useState(undefined);
|
|
80
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
81
|
+
const generationStartRef = useRef(undefined);
|
|
80
82
|
const [statusLines, setStatusLines] = useState([]);
|
|
81
83
|
// Per-message live streaming buffers
|
|
82
84
|
const [liveBuffers, setLiveBuffers] = useState(new Map());
|
|
@@ -120,6 +122,8 @@ export default function Code({ cli }) {
|
|
|
120
122
|
setIsDownloading(false);
|
|
121
123
|
setDownloadedPath(undefined);
|
|
122
124
|
setDownloadError(undefined);
|
|
125
|
+
generationStartRef.current = Date.now();
|
|
126
|
+
setElapsedSeconds(0);
|
|
123
127
|
setIsGenerating(true);
|
|
124
128
|
const controller = new AbortController();
|
|
125
129
|
let didTimeout = false;
|
|
@@ -300,6 +304,9 @@ export default function Code({ cli }) {
|
|
|
300
304
|
}
|
|
301
305
|
finally {
|
|
302
306
|
clearTimeout(timeoutId);
|
|
307
|
+
if (generationStartRef.current !== undefined) {
|
|
308
|
+
setElapsedSeconds(Math.floor((Date.now() - generationStartRef.current) / 1000));
|
|
309
|
+
}
|
|
303
310
|
setIsGenerating(false);
|
|
304
311
|
}
|
|
305
312
|
}
|
|
@@ -307,6 +314,23 @@ export default function Code({ cli }) {
|
|
|
307
314
|
runCodeGeneration(prompt, appName);
|
|
308
315
|
}
|
|
309
316
|
}, [prompt, appName, cli.flags.stage, isAuthenticated, token, continueData]);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!isGenerating)
|
|
319
|
+
return;
|
|
320
|
+
const interval = setInterval(() => {
|
|
321
|
+
if (generationStartRef.current !== undefined) {
|
|
322
|
+
setElapsedSeconds(Math.floor((Date.now() - generationStartRef.current) / 1000));
|
|
323
|
+
}
|
|
324
|
+
}, 1000);
|
|
325
|
+
return () => clearInterval(interval);
|
|
326
|
+
}, [isGenerating]);
|
|
327
|
+
function formatElapsed(seconds) {
|
|
328
|
+
if (seconds < 60)
|
|
329
|
+
return `${seconds}s`;
|
|
330
|
+
const m = Math.floor(seconds / 60);
|
|
331
|
+
const s = seconds % 60;
|
|
332
|
+
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
333
|
+
}
|
|
310
334
|
function getLiveLines(messageId, toolTitle) {
|
|
311
335
|
const text = liveBuffers.get(messageId);
|
|
312
336
|
if (!text)
|
|
@@ -333,10 +357,10 @@ export default function Code({ cli }) {
|
|
|
333
357
|
setPrompt(trimmed);
|
|
334
358
|
} })] })) : (
|
|
335
359
|
// Step 3 — generating
|
|
336
|
-
_jsxs(_Fragment, { children: [cli.flags.continue && continueData && (_jsxs(Text, { dimColor: true, children: ["Continuing from v", continueData.version, " (", continueData.appId, ")"] })), _jsxs(Text, { children: ["App name: ", _jsx(Text, { color: "yellow", children: appName })] }), _jsxs(Text, { children: ["Prompt: ", _jsx(Text, { color: "cyan", children: prompt })] }), isGenerating && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Generating app..." })] })), statusLines.map((item, index) => {
|
|
360
|
+
_jsxs(_Fragment, { children: [cli.flags.continue && continueData && (_jsxs(Text, { dimColor: true, children: ["Continuing from v", continueData.version, " (", continueData.appId, ")"] })), _jsxs(Text, { children: ["App name: ", _jsx(Text, { color: "yellow", children: appName })] }), _jsxs(Text, { children: ["Prompt: ", _jsx(Text, { color: "cyan", children: prompt })] }), isGenerating && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Generating app... " }), _jsxs(Text, { color: "gray", children: ["[", formatElapsed(elapsedSeconds), "]"] })] })), statusLines.map((item, index) => {
|
|
337
361
|
const { lines, truncated } = item.messageId !== undefined
|
|
338
362
|
? getLiveLines(item.messageId, item.toolTitle)
|
|
339
363
|
: { lines: [], truncated: false };
|
|
340
364
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: index < statusLines.length - 1, children: ["\u2022 ", item.text] }), lines.length > 0 && (_jsxs(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [truncated && (_jsx(Text, { color: "gray", dimColor: true, children: "<truncated>" })), lines.map((line, lineIndex) => (_jsx(Text, { color: "gray", children: line }, lineIndex)))] }))] }, `${item.text}-${index}`));
|
|
341
|
-
}), toolCallErrors.map((err, index) => (_jsxs(Text, { color: "redBright", children: ["\u274C ", err] }, index))), error && _jsxs(Text, { color: "redBright", children: ["\u274C ", error] }), isGenerated && !error && (
|
|
365
|
+
}), toolCallErrors.map((err, index) => (_jsxs(Text, { color: "redBright", children: ["\u274C ", err] }, index))), error && _jsxs(Text, { color: "redBright", children: ["\u274C ", error] }), isGenerated && !error && (_jsxs(Text, { color: "greenBright", children: ["\u2705 Code generation completed in ", formatElapsed(elapsedSeconds), "."] })), artifact?.publishedAppLink && (_jsxs(Text, { color: "greenBright", children: ["Preview: ", artifact.publishedAppLink] })), artifact?.sourceCodeArchiveLink && (_jsxs(Text, { color: "greenBright", children: ["Source (.zip): ", artifact.sourceCodeArchiveLink] })), isDownloading && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Downloading source archive..." })] })), downloadedPath && (_jsxs(Text, { color: "greenBright", children: ["Source extracted: ", downloadedPath] })), downloadError && (_jsxs(Text, { color: "redBright", children: ["\u274C Download failed: ", downloadError] }))] })) }));
|
|
342
366
|
}
|
|
@@ -97,6 +97,15 @@ app.all(/^\/server-function\/(.*)/, async (req, res) => {
|
|
|
97
97
|
});
|
|
98
98
|
if (isPreview) {
|
|
99
99
|
/* Preview mode */
|
|
100
|
+
app.get("/pulse.config.json", async (_req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const data = await import("fs/promises").then((fs) => fs.readFile("dist/pulse.config.json", "utf-8"));
|
|
103
|
+
res.type("json").send(data);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
res.status(404).json({ error: "pulse.config.json not found" });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
100
109
|
app.use(express.static("dist/client"));
|
|
101
110
|
// Expose skill actions as REST API endpoints in dev and preview modes
|
|
102
111
|
const skillActions = pulseConfig?.actions || [];
|
|
@@ -1,10 +1,67 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/* This folder contains temporary code to be moved to a different package in the future. */
|
|
3
|
+
import React from 'react';
|
|
3
4
|
import ReactDOM from 'react-dom/client';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return
|
|
5
|
+
function ErrorPage({ error }) {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
8
|
+
return (_jsxs("div", { style: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
height: '100vh',
|
|
14
|
+
width: '100vw',
|
|
15
|
+
fontFamily: 'monospace',
|
|
16
|
+
backgroundColor: '#1a1a1a',
|
|
17
|
+
color: '#ff6b6b',
|
|
18
|
+
padding: '2rem',
|
|
19
|
+
boxSizing: 'border-box',
|
|
20
|
+
}, children: [_jsx("div", { style: { fontSize: '3rem', marginBottom: '1rem' }, children: "\u26A0" }), _jsx("h1", { style: { margin: '0 0 0.5rem', fontSize: '1.5rem', color: '#ff6b6b' }, children: "Rendering Error" }), _jsx("p", { style: { margin: '0 0 1.5rem', color: '#aaa', fontSize: '0.9rem' }, children: "An error occurred while rendering the preview." }), _jsxs("pre", { style: {
|
|
21
|
+
background: '#2a2a2a',
|
|
22
|
+
border: '1px solid #444',
|
|
23
|
+
borderRadius: '6px',
|
|
24
|
+
padding: '1rem',
|
|
25
|
+
maxWidth: '100%',
|
|
26
|
+
overflowX: 'auto',
|
|
27
|
+
color: '#ff9999',
|
|
28
|
+
fontSize: '0.85rem',
|
|
29
|
+
whiteSpace: 'pre-wrap',
|
|
30
|
+
wordBreak: 'break-word',
|
|
31
|
+
}, children: [message, stack ? `\n\n${stack}` : ''] })] }));
|
|
32
|
+
}
|
|
33
|
+
class ErrorBoundary extends React.Component {
|
|
34
|
+
constructor(props) {
|
|
35
|
+
super(props);
|
|
36
|
+
this.state = { error: null };
|
|
37
|
+
}
|
|
38
|
+
static getDerivedStateFromError(error) {
|
|
39
|
+
return { error };
|
|
40
|
+
}
|
|
41
|
+
render() {
|
|
42
|
+
if (this.state.error) {
|
|
43
|
+
return _jsx(ErrorPage, { error: this.state.error });
|
|
44
|
+
}
|
|
45
|
+
return this.props.children;
|
|
46
|
+
}
|
|
8
47
|
}
|
|
9
48
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
10
|
-
|
|
49
|
+
function showError(error) {
|
|
50
|
+
root.render(_jsx(ErrorPage, { error: error }));
|
|
51
|
+
}
|
|
52
|
+
// Fallback: catch errors that slip past the React error boundary
|
|
53
|
+
// (e.g. React 18 re-throws initial-render errors as uncaught)
|
|
54
|
+
window.addEventListener('error', (event) => {
|
|
55
|
+
showError(event.error ?? new Error(event.message));
|
|
56
|
+
});
|
|
57
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
58
|
+
showError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)));
|
|
59
|
+
});
|
|
60
|
+
// Use dynamic import so module-level errors in main.tsx are also caught
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
import("../../../../../src/main.tsx")
|
|
63
|
+
.then((mod) => {
|
|
64
|
+
const Main = mod.default;
|
|
65
|
+
root.render(_jsx(ErrorBoundary, { children: _jsx(Main, {}) }));
|
|
66
|
+
})
|
|
67
|
+
.catch(showError);
|
|
@@ -33,13 +33,13 @@ class MFClientPlugin {
|
|
|
33
33
|
compiler.hooks.invalid.tap("LogFileUpdates", (file, changeTime) => {
|
|
34
34
|
console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
|
|
35
35
|
});
|
|
36
|
-
const devStartupMessage = `
|
|
37
|
-
🎉 Your Pulse extension \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
|
|
38
|
-
|
|
39
|
-
⚡️ Local: http://localhost:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
40
|
-
⚡️ Network: http://${this.origin}:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
41
|
-
|
|
42
|
-
✨ Try it out in the Pulse Editor and let the magic happen! 🚀
|
|
36
|
+
const devStartupMessage = `
|
|
37
|
+
🎉 Your Pulse extension \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
|
|
38
|
+
|
|
39
|
+
⚡️ Local: http://localhost:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
40
|
+
⚡️ Network: http://${this.origin}:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
41
|
+
|
|
42
|
+
✨ Try it out in the Pulse Editor and let the magic happen! 🚀
|
|
43
43
|
`;
|
|
44
44
|
// After build finishes
|
|
45
45
|
compiler.hooks.done.tap("ReloadMessagePlugin", () => {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import mfNode from "@module-federation/node";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import { globSync } from "glob";
|
|
5
4
|
import path from "path";
|
|
6
|
-
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
7
5
|
import wp from "webpack";
|
|
8
|
-
import { discoverAppSkillActions, discoverServerFunctions, loadPulseConfig, } from "./utils.js";
|
|
6
|
+
import { compileAppActionSkills, discoverAppSkillActions, discoverServerFunctions, loadPulseConfig, } from "./utils.js";
|
|
9
7
|
const { NodeFederationPlugin } = mfNode;
|
|
10
8
|
const { webpack } = wp;
|
|
11
9
|
class MFServerPlugin {
|
|
@@ -48,21 +46,21 @@ class MFServerPlugin {
|
|
|
48
46
|
: false;
|
|
49
47
|
if (isActionChange) {
|
|
50
48
|
console.log(`[Server] Detected changes in actions. Recompiling...`);
|
|
51
|
-
this.
|
|
49
|
+
compileAppActionSkills(this.pulseConfig);
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
else {
|
|
55
53
|
console.log(`[Server] 🔄 Building app...`);
|
|
56
54
|
await this.compileServerFunctions(compiler);
|
|
57
|
-
this.
|
|
55
|
+
compileAppActionSkills(this.pulseConfig);
|
|
58
56
|
console.log(`[Server] ✅ Successfully built server.`);
|
|
59
57
|
const funcs = discoverServerFunctions();
|
|
60
|
-
console.log(`\n🛜 Server functions:
|
|
58
|
+
console.log(`\n🛜 Server functions:
|
|
61
59
|
${Object.entries(funcs)
|
|
62
60
|
.map(([name, file]) => {
|
|
63
61
|
return ` - ${name.slice(2)} (from ${file})`;
|
|
64
62
|
})
|
|
65
|
-
.join("\n")}
|
|
63
|
+
.join("\n")}
|
|
66
64
|
`);
|
|
67
65
|
}
|
|
68
66
|
});
|
|
@@ -85,7 +83,7 @@ ${Object.entries(funcs)
|
|
|
85
83
|
else {
|
|
86
84
|
try {
|
|
87
85
|
await this.compileServerFunctions(compiler);
|
|
88
|
-
this.
|
|
86
|
+
compileAppActionSkills(this.pulseConfig);
|
|
89
87
|
}
|
|
90
88
|
catch (err) {
|
|
91
89
|
console.error(`[Server] ❌ Error during compilation:`, err);
|
|
@@ -158,252 +156,6 @@ ${Object.entries(funcs)
|
|
|
158
156
|
},
|
|
159
157
|
}, {});
|
|
160
158
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Register default functions defined in src/skill as exposed modules in Module Federation.
|
|
163
|
-
* This will:
|
|
164
|
-
* 1. Search for all .ts files under src/skill
|
|
165
|
-
* 2. Use ts-morph to get the default function information, including function name, parameters, and JSDoc comments
|
|
166
|
-
* 3. Organize the functions' information into a list of Action
|
|
167
|
-
* @param compiler
|
|
168
|
-
*/
|
|
169
|
-
compileAppActionSkills() {
|
|
170
|
-
// 1. Get all TypeScript files under src/skill
|
|
171
|
-
const files = globSync("./src/skill/*/action.ts");
|
|
172
|
-
const project = new Project({
|
|
173
|
-
tsConfigFilePath: path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json"),
|
|
174
|
-
});
|
|
175
|
-
const actions = [];
|
|
176
|
-
files.forEach((file) => {
|
|
177
|
-
const sourceFile = project.addSourceFileAtPath(file);
|
|
178
|
-
const defaultExportSymbol = sourceFile.getDefaultExportSymbol();
|
|
179
|
-
if (!defaultExportSymbol)
|
|
180
|
-
return;
|
|
181
|
-
const defaultExportDeclarations = defaultExportSymbol.getDeclarations();
|
|
182
|
-
defaultExportDeclarations.forEach((declaration) => {
|
|
183
|
-
if (declaration.getKind() !== SyntaxKind.FunctionDeclaration)
|
|
184
|
-
return;
|
|
185
|
-
const funcDecl = declaration.asKindOrThrow(SyntaxKind.FunctionDeclaration);
|
|
186
|
-
// Get action name from path `src/skill/{actionName}/action.ts`
|
|
187
|
-
// Match `*/src/skill/{actionName}/action.ts` and extract {actionName}
|
|
188
|
-
const pattern = /src\/skill\/([^\/]+)\/action\.ts$/;
|
|
189
|
-
const match = file.replaceAll("\\", "/").match(pattern);
|
|
190
|
-
if (!match) {
|
|
191
|
-
console.warn(`File path ${file} does not match pattern ${pattern}. Skipping...`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const actionName = match[1];
|
|
195
|
-
if (!actionName) {
|
|
196
|
-
console.warn(`Could not extract action name from file path ${file}. Skipping...`);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// Throw an error if the funcName is duplicated with an existing action to prevent accidental overwriting
|
|
200
|
-
if (actions.some((action) => action.name === actionName)) {
|
|
201
|
-
throw new Error(`Duplicate action name "${actionName}" detected in file ${file}. Please ensure all actions have unique names to avoid conflicts.`);
|
|
202
|
-
}
|
|
203
|
-
const defaultExportJSDocs = funcDecl.getJsDocs();
|
|
204
|
-
// Validate that the function has a JSDoc description
|
|
205
|
-
const descriptionText = defaultExportJSDocs
|
|
206
|
-
.map((doc) => doc.getDescription().replace(/^\*+/gm, "").trim())
|
|
207
|
-
.join("\n")
|
|
208
|
-
.trim();
|
|
209
|
-
if (defaultExportJSDocs.length === 0 || !descriptionText) {
|
|
210
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file} is missing a JSDoc description. ` +
|
|
211
|
-
`Please add a JSDoc comment block with a description above the function.` +
|
|
212
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
213
|
-
}
|
|
214
|
-
const description = defaultExportJSDocs
|
|
215
|
-
.map((doc) => doc.getFullText())
|
|
216
|
-
.join("\n");
|
|
217
|
-
const allJSDocs = sourceFile.getDescendantsOfKind(SyntaxKind.JSDoc);
|
|
218
|
-
const typeDefs = this.parseTypeDefs(allJSDocs);
|
|
219
|
-
/* Extract parameter descriptions from JSDoc */
|
|
220
|
-
const funcParam = funcDecl.getParameters()[0];
|
|
221
|
-
const params = {};
|
|
222
|
-
if (funcParam) {
|
|
223
|
-
/**
|
|
224
|
-
* Extract default values from the destructured parameter
|
|
225
|
-
* (ObjectBindingPattern → BindingElement initializer)
|
|
226
|
-
*/
|
|
227
|
-
const defaults = new Map();
|
|
228
|
-
const nameNode = funcParam.getNameNode();
|
|
229
|
-
if (Node.isObjectBindingPattern(nameNode)) {
|
|
230
|
-
nameNode.getElements().forEach((el) => {
|
|
231
|
-
if (!Node.isBindingElement(el))
|
|
232
|
-
return;
|
|
233
|
-
const name = el.getName();
|
|
234
|
-
const initializer = el.getInitializer()?.getText();
|
|
235
|
-
if (initializer) {
|
|
236
|
-
defaults.set(name, initializer);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
const paramProperties = funcParam.getType().getProperties();
|
|
241
|
-
const inputTypeDef = typeDefs["input"] ?? {};
|
|
242
|
-
if (paramProperties.length > 0 && !typeDefs["input"]) {
|
|
243
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file} has parameters but is missing an ` +
|
|
244
|
-
`"@typedef {Object} input" JSDoc block. Please document all parameters with ` +
|
|
245
|
-
`@typedef {Object} input and @property tags.` +
|
|
246
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
247
|
-
}
|
|
248
|
-
paramProperties.forEach((prop) => {
|
|
249
|
-
const name = prop.getName();
|
|
250
|
-
if (!inputTypeDef[name]) {
|
|
251
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file}: parameter "${name}" is missing ` +
|
|
252
|
-
`a @property entry in the "input" JSDoc typedef. Please add ` +
|
|
253
|
-
`"@property {type} ${name} - description" to the JSDoc.` +
|
|
254
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
255
|
-
}
|
|
256
|
-
if (!inputTypeDef[name]?.description?.trim()) {
|
|
257
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file}: parameter "${name}" has an empty ` +
|
|
258
|
-
`description in the JSDoc @property. Please provide a meaningful description.` +
|
|
259
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
260
|
-
}
|
|
261
|
-
const variable = {
|
|
262
|
-
description: inputTypeDef[name]?.description ?? "",
|
|
263
|
-
type: this.getType(inputTypeDef[name]?.type ?? ""),
|
|
264
|
-
optional: prop.isOptional() ? true : undefined,
|
|
265
|
-
defaultValue: defaults.get(name),
|
|
266
|
-
};
|
|
267
|
-
params[name] = variable;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
/* Extract return type from JSDoc */
|
|
271
|
-
const rawReturnType = funcDecl.getReturnType();
|
|
272
|
-
const isPromiseLikeReturn = this.isPromiseLikeType(rawReturnType);
|
|
273
|
-
const returnType = this.unwrapPromiseLikeType(rawReturnType);
|
|
274
|
-
// Check if the return type is an object
|
|
275
|
-
if (!returnType.isObject()) {
|
|
276
|
-
console.warn(`[Action Registration] Function ${actionName}'s return type should be an object. Skipping...`);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const returns = {};
|
|
280
|
-
const returnProperties = returnType.getProperties();
|
|
281
|
-
const outputTypeDef = typeDefs["output"] ?? {};
|
|
282
|
-
const hasOutputTypeDef = !!typeDefs["output"];
|
|
283
|
-
if (returnProperties.length > 0 &&
|
|
284
|
-
!hasOutputTypeDef &&
|
|
285
|
-
!isPromiseLikeReturn) {
|
|
286
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file} returns properties but is missing an ` +
|
|
287
|
-
`"@typedef {Output}" JSDoc block. Please document all return values with ` +
|
|
288
|
-
`@typedef {Output} and @property tags.` +
|
|
289
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
290
|
-
}
|
|
291
|
-
if (returnProperties.length > 0 && !hasOutputTypeDef && isPromiseLikeReturn) {
|
|
292
|
-
console.warn(`[Action Validation] Action "${actionName}" in ${file} is missing an "@typedef {Object} output" JSDoc block. ` +
|
|
293
|
-
`Falling back to TypeScript-inferred return metadata because the action returns a Promise.`);
|
|
294
|
-
}
|
|
295
|
-
returnProperties.forEach((prop) => {
|
|
296
|
-
const name = prop.getName();
|
|
297
|
-
if (!hasOutputTypeDef) {
|
|
298
|
-
const variable = {
|
|
299
|
-
description: "",
|
|
300
|
-
type: this.getType(prop.getTypeAtLocation(funcDecl).getText()),
|
|
301
|
-
optional: prop.isOptional() ? true : undefined,
|
|
302
|
-
defaultValue: undefined,
|
|
303
|
-
};
|
|
304
|
-
returns[name] = variable;
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (!outputTypeDef[name]) {
|
|
308
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file}: return property "${name}" is missing ` +
|
|
309
|
-
`a @property entry in the "output" JSDoc typedef. Please add ` +
|
|
310
|
-
`"@property {type} ${name} - description" to the JSDoc.` +
|
|
311
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
312
|
-
}
|
|
313
|
-
if (!outputTypeDef[name]?.description?.trim()) {
|
|
314
|
-
throw new Error(`[Action Validation] Action "${actionName}" in ${file}: return property "${name}" has an empty ` +
|
|
315
|
-
`description in the JSDoc @property. Please provide a meaningful description.` +
|
|
316
|
-
`Run \`pulse skill fix ${actionName}\` to automatically add a JSDoc template for this action.`);
|
|
317
|
-
}
|
|
318
|
-
const variable = {
|
|
319
|
-
description: outputTypeDef[name]?.description ?? "",
|
|
320
|
-
type: this.getType(outputTypeDef[name]?.type ?? ""),
|
|
321
|
-
optional: prop.isOptional() ? true : undefined,
|
|
322
|
-
defaultValue: undefined,
|
|
323
|
-
};
|
|
324
|
-
returns[name] = variable;
|
|
325
|
-
});
|
|
326
|
-
actions.push({
|
|
327
|
-
name: actionName,
|
|
328
|
-
description,
|
|
329
|
-
parameters: params,
|
|
330
|
-
returns,
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
// You can now register `actions` in Module Federation or expose them as needed
|
|
335
|
-
// Register actions in pulse config for runtime access
|
|
336
|
-
this.pulseConfig.actions = actions;
|
|
337
|
-
}
|
|
338
|
-
parseTypeDefs(jsDocs) {
|
|
339
|
-
const typeDefs = {};
|
|
340
|
-
// Match @typedef {Type} Name
|
|
341
|
-
const typedefRegex = /@typedef\s+{([^}]+)}\s+([^\s-]+)/g;
|
|
342
|
-
// Match @property {Type} [name] Description text...
|
|
343
|
-
const propertyRegex = /@property\s+{([^}]+)}\s+(\[?[^\]\s]+\]?)\s*-?\s*(.*)/g;
|
|
344
|
-
jsDocs.forEach((doc) => {
|
|
345
|
-
const text = doc.getFullText();
|
|
346
|
-
let typedefMatches;
|
|
347
|
-
while ((typedefMatches = typedefRegex.exec(text)) !== null) {
|
|
348
|
-
const typeName = typedefMatches[2];
|
|
349
|
-
if (!typeName)
|
|
350
|
-
continue;
|
|
351
|
-
const properties = {};
|
|
352
|
-
let propertyMatches;
|
|
353
|
-
while ((propertyMatches = propertyRegex.exec(text)) !== null) {
|
|
354
|
-
const propName = this.normalizeJSDocPropertyName(propertyMatches[2]);
|
|
355
|
-
const propType = propertyMatches[1];
|
|
356
|
-
const propDescription = propertyMatches[3] || "";
|
|
357
|
-
if (propName && propType) {
|
|
358
|
-
properties[propName] = {
|
|
359
|
-
type: propType,
|
|
360
|
-
description: propDescription.trim(),
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
typeDefs[typeName.toLowerCase()] = properties;
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
return typeDefs;
|
|
368
|
-
}
|
|
369
|
-
normalizeJSDocPropertyName(name) {
|
|
370
|
-
if (!name)
|
|
371
|
-
return "";
|
|
372
|
-
return name
|
|
373
|
-
.trim()
|
|
374
|
-
.replace(/^\[/, "")
|
|
375
|
-
.replace(/\]$/, "")
|
|
376
|
-
.split("=")[0]
|
|
377
|
-
?.trim();
|
|
378
|
-
}
|
|
379
|
-
isPromiseLikeType(type) {
|
|
380
|
-
const symbolName = type.getSymbol()?.getName();
|
|
381
|
-
return symbolName === "Promise" || symbolName === "PromiseLike";
|
|
382
|
-
}
|
|
383
|
-
unwrapPromiseLikeType(type) {
|
|
384
|
-
const symbolName = type.getSymbol()?.getName();
|
|
385
|
-
if ((symbolName === "Promise" || symbolName === "PromiseLike") &&
|
|
386
|
-
type.getTypeArguments().length > 0) {
|
|
387
|
-
return type.getTypeArguments()[0] ?? type;
|
|
388
|
-
}
|
|
389
|
-
return type;
|
|
390
|
-
}
|
|
391
|
-
getType(text) {
|
|
392
|
-
if (text === "string")
|
|
393
|
-
return "string";
|
|
394
|
-
if (text === "number")
|
|
395
|
-
return "number";
|
|
396
|
-
if (text === "boolean")
|
|
397
|
-
return "boolean";
|
|
398
|
-
if (text === "any")
|
|
399
|
-
return "object";
|
|
400
|
-
if (text.endsWith("[]"))
|
|
401
|
-
return [this.getType(text.slice(0, -2))];
|
|
402
|
-
if (text.length === 0)
|
|
403
|
-
return "undefined";
|
|
404
|
-
console.warn(`[Type Warning] Unrecognized type "${text}". Consider adding explicit types in your action's JSDoc comments for better type safety and documentation.`);
|
|
405
|
-
return text;
|
|
406
|
-
}
|
|
407
159
|
printChanges(compiler) {
|
|
408
160
|
const modified = compiler.modifiedFiles
|
|
409
161
|
? Array.from(compiler.modifiedFiles)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { Configuration as WebpackConfig } from "webpack";
|
|
2
2
|
import { Configuration as DevServerConfig } from "webpack-dev-server";
|
|
3
|
+
export declare function makeMFPreviewConfig(): Promise<WebpackConfig>;
|
|
3
4
|
export declare function makePreviewClientConfig(mode: "development" | "production"): Promise<WebpackConfig & DevServerConfig>;
|
|
@@ -1,10 +1,133 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import mfNode from "@module-federation/node";
|
|
2
3
|
import CopyWebpackPlugin from "copy-webpack-plugin";
|
|
3
4
|
import fs from "fs";
|
|
4
5
|
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
5
6
|
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|
6
7
|
import path from "path";
|
|
7
|
-
import
|
|
8
|
+
import wp from "webpack";
|
|
9
|
+
import { compileAppActionSkills, discoverAppSkillActions, getLocalNetworkIP, loadPulseConfig, } from "./utils.js";
|
|
10
|
+
const { NodeFederationPlugin } = mfNode;
|
|
11
|
+
const { webpack } = wp;
|
|
12
|
+
class MFPreviewPlugin {
|
|
13
|
+
projectDirName;
|
|
14
|
+
pulseConfig;
|
|
15
|
+
constructor(pulseConfig) {
|
|
16
|
+
this.projectDirName = process.cwd();
|
|
17
|
+
this.pulseConfig = pulseConfig;
|
|
18
|
+
}
|
|
19
|
+
apply(compiler) {
|
|
20
|
+
let isFirstRun = true;
|
|
21
|
+
compiler.hooks.environment.tap("WatchSkillPlugin", () => {
|
|
22
|
+
compiler.hooks.thisCompilation.tap("WatchActions", (compilation) => {
|
|
23
|
+
compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/skill"));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
compiler.hooks.watchRun.tap("MFPreviewPlugin", async () => {
|
|
27
|
+
if (!isFirstRun) {
|
|
28
|
+
const isActionChange = compiler.modifiedFiles
|
|
29
|
+
? Array.from(compiler.modifiedFiles).some((file) => file.includes("src/skill"))
|
|
30
|
+
: false;
|
|
31
|
+
if (isActionChange) {
|
|
32
|
+
console.log("[preview-server] Detected changes in actions. Recompiling...");
|
|
33
|
+
await this.compileActions(compiler);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log("[preview-server] 🔄 Building...");
|
|
38
|
+
await this.compileActions(compiler);
|
|
39
|
+
console.log("[preview-server] ✅ Successfully built preview server.");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
compiler.hooks.done.tap("MFPreviewPlugin", () => {
|
|
43
|
+
if (isFirstRun) {
|
|
44
|
+
isFirstRun = false;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log("[preview-server] ✅ Reload finished.");
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
makeNodeFederationPlugin() {
|
|
53
|
+
const actions = discoverAppSkillActions();
|
|
54
|
+
return new NodeFederationPlugin({
|
|
55
|
+
name: this.pulseConfig.id + "_server",
|
|
56
|
+
remoteType: "script",
|
|
57
|
+
useRuntimePlugin: true,
|
|
58
|
+
library: { type: "commonjs-module" },
|
|
59
|
+
filename: "remoteEntry.js",
|
|
60
|
+
exposes: { ...actions },
|
|
61
|
+
}, {});
|
|
62
|
+
}
|
|
63
|
+
async compileActions(compiler) {
|
|
64
|
+
compileAppActionSkills(this.pulseConfig);
|
|
65
|
+
const options = {
|
|
66
|
+
...compiler.options,
|
|
67
|
+
watch: false,
|
|
68
|
+
plugins: [this.makeNodeFederationPlugin()],
|
|
69
|
+
};
|
|
70
|
+
const newCompiler = webpack(options);
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
newCompiler?.run((err, stats) => {
|
|
73
|
+
if (err) {
|
|
74
|
+
console.error("[preview-server] ❌ Error during recompilation:", err);
|
|
75
|
+
reject(err);
|
|
76
|
+
}
|
|
77
|
+
else if (stats?.hasErrors()) {
|
|
78
|
+
console.error("[preview-server] ❌ Compilation errors:", stats.toJson().errors);
|
|
79
|
+
reject(new Error("Compilation errors"));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log("[preview-server] ✅ Compiled actions successfully.");
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function makeMFPreviewConfig() {
|
|
90
|
+
const projectDirName = process.cwd();
|
|
91
|
+
const pulseConfig = await loadPulseConfig();
|
|
92
|
+
return {
|
|
93
|
+
mode: "development",
|
|
94
|
+
name: "preview-server",
|
|
95
|
+
entry: {},
|
|
96
|
+
target: "async-node",
|
|
97
|
+
output: {
|
|
98
|
+
publicPath: "auto",
|
|
99
|
+
path: path.resolve(projectDirName, "dist/server"),
|
|
100
|
+
},
|
|
101
|
+
resolve: {
|
|
102
|
+
extensions: [".ts", ".js"],
|
|
103
|
+
},
|
|
104
|
+
plugins: [new MFPreviewPlugin(pulseConfig)],
|
|
105
|
+
module: {
|
|
106
|
+
rules: [
|
|
107
|
+
{
|
|
108
|
+
test: /\.tsx?$/,
|
|
109
|
+
use: {
|
|
110
|
+
loader: "ts-loader",
|
|
111
|
+
options: {
|
|
112
|
+
configFile: "node_modules/.pulse/tsconfig.server.json",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
exclude: [/node_modules/, /dist/],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
stats: {
|
|
120
|
+
all: false,
|
|
121
|
+
errors: true,
|
|
122
|
+
warnings: true,
|
|
123
|
+
logging: "warn",
|
|
124
|
+
colors: true,
|
|
125
|
+
},
|
|
126
|
+
infrastructureLogging: {
|
|
127
|
+
level: "warn",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
8
131
|
class PreviewClientPlugin {
|
|
9
132
|
projectDirName;
|
|
10
133
|
pulseConfig;
|
|
@@ -28,13 +151,13 @@ class PreviewClientPlugin {
|
|
|
28
151
|
// After build finishes
|
|
29
152
|
compiler.hooks.done.tap("ReloadMessagePlugin", () => {
|
|
30
153
|
if (isFirstRun) {
|
|
31
|
-
const previewStartupMessage = `
|
|
32
|
-
🎉 Your Pulse extension preview \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
|
|
33
|
-
|
|
34
|
-
⚡️ Local: http://localhost:3030
|
|
35
|
-
⚡️ Network: http://${this.origin}:3030
|
|
36
|
-
|
|
37
|
-
✨ Try it out in your browser and let the magic happen! 🚀
|
|
154
|
+
const previewStartupMessage = `
|
|
155
|
+
🎉 Your Pulse extension preview \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
|
|
156
|
+
|
|
157
|
+
⚡️ Local: http://localhost:3030
|
|
158
|
+
⚡️ Network: http://${this.origin}:3030
|
|
159
|
+
|
|
160
|
+
✨ Try it out in your browser and let the magic happen! 🚀
|
|
38
161
|
`;
|
|
39
162
|
console.log("[client-preview] ✅ Successfully built preview.");
|
|
40
163
|
const skillActions = this.pulseConfig?.actions || [];
|
|
@@ -50,8 +173,6 @@ class PreviewClientPlugin {
|
|
|
50
173
|
else {
|
|
51
174
|
console.log("[client-preview] ✅ Reload finished");
|
|
52
175
|
}
|
|
53
|
-
// Write pulse config to dist
|
|
54
|
-
fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
|
|
55
176
|
});
|
|
56
177
|
}
|
|
57
178
|
}
|