@pulse-editor/cli 0.1.3 → 0.1.4

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.
@@ -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 && (_jsx(Text, { color: "greenBright", children: "\u2705 Code generation completed." })), 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] }))] })) }));
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,9 @@ app.all(/^\/server-function\/(.*)/, async (req, res) => {
97
97
  });
98
98
  if (isPreview) {
99
99
  /* Preview mode */
100
+ app.get("/pulse.config.json", (_req, res) => {
101
+ res.sendFile(path.resolve("dist/pulse.config.json"));
102
+ });
100
103
  app.use(express.static("dist/client"));
101
104
  // Expose skill actions as REST API endpoints in dev and preview modes
102
105
  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
- // @ts-expect-error ignore ts error for now
5
- import Main from "../../../../../src/main.tsx";
6
- function Preview() {
7
- return _jsx(Main, {});
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
- root.render(_jsx(Preview, {}));
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", () => {
@@ -57,12 +57,12 @@ class MFServerPlugin {
57
57
  this.compileAppActionSkills();
58
58
  console.log(`[Server] ✅ Successfully built server.`);
59
59
  const funcs = discoverServerFunctions();
60
- console.log(`\n🛜 Server functions:
60
+ console.log(`\n🛜 Server functions:
61
61
  ${Object.entries(funcs)
62
62
  .map(([name, file]) => {
63
63
  return ` - ${name.slice(2)} (from ${file})`;
64
64
  })
65
- .join("\n")}
65
+ .join("\n")}
66
66
  `);
67
67
  }
68
68
  });
@@ -28,13 +28,13 @@ class PreviewClientPlugin {
28
28
  // After build finishes
29
29
  compiler.hooks.done.tap("ReloadMessagePlugin", () => {
30
30
  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! 🚀
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! 🚀
38
38
  `;
39
39
  console.log("[client-preview] ✅ Successfully built preview.");
40
40
  const skillActions = this.pulseConfig?.actions || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-editor/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "pulse": "dist/cli.js"
@@ -1 +0,0 @@
1
- export declare function readConfigFile(): Promise<any>;
@@ -1,17 +0,0 @@
1
- import fs from 'fs/promises';
2
- export async function readConfigFile() {
3
- // Read pulse.config.json from dist/client
4
- // Wait until dist/pulse.config.json exists
5
- while (true) {
6
- try {
7
- await fs.access('dist/pulse.config.json');
8
- break;
9
- }
10
- catch (err) {
11
- // Wait for 100ms before trying again
12
- await new Promise(resolve => setTimeout(resolve, 100));
13
- }
14
- }
15
- const data = await fs.readFile('dist/pulse.config.json', 'utf-8');
16
- return JSON.parse(data);
17
- }
@@ -1,2 +0,0 @@
1
- import { Action } from "@pulse-editor/shared-utils";
2
- export declare const preRegisteredActions: Record<string, Action>;
@@ -1,7 +0,0 @@
1
- import { AppConfig } from "@pulse-editor/shared-utils";
2
- /**
3
- * Pulse Editor Extension Config
4
- *
5
- */
6
- declare const config: AppConfig;
7
- export default config;
@@ -1,2 +0,0 @@
1
- import { ChatPromptTemplate } from "@langchain/core/prompts";
2
- export declare const codeModifierAgentPrompt: ChatPromptTemplate<any, any>;
@@ -1,3 +0,0 @@
1
- import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
- import { MessageStreamController } from "../streaming/message-stream-controller";
3
- export declare function runVibeCoding(mcpClient: MultiServerMCPClient, userPrompt: string, controller: MessageStreamController): Promise<void>;
@@ -1,3 +0,0 @@
1
- import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
- export declare function writeFileToFS(mcpClient: MultiServerMCPClient, uri: string, content: string): Promise<string>;
3
- export declare function callTerminal(mcpClient: MultiServerMCPClient, command: string): Promise<string>;
@@ -1,10 +0,0 @@
1
- import { AgentTaskMessageData } from "../types";
2
- export declare class MessageStreamController {
3
- private controller;
4
- private msgCounter;
5
- private messages;
6
- private timeCountMap;
7
- constructor(controller: ReadableStreamDefaultController);
8
- enqueueNew(data: AgentTaskMessageData, isFinal: boolean): number;
9
- enqueueUpdate(data: AgentTaskMessageData, isFinal: boolean): void;
10
- }
@@ -1,58 +0,0 @@
1
- export type VibeDevFlowNode = {
2
- id: string;
3
- children: VibeDevFlowNode[];
4
- };
5
- export declare enum AgentTaskMessageType {
6
- Creation = "creation",
7
- Update = "update"
8
- }
9
- export declare enum AgentTaskMessageDataType {
10
- Notification = "notification",
11
- ToolCall = "toolCall",
12
- ArtifactOutput = "artifactOutput"
13
- }
14
- /**
15
- * Data associated with an AgentTaskItem.
16
- * The fields included depend on the type of the task item.
17
- *
18
- * @property type - The type of the task item, defined by the AgentTaskItemType enum.
19
- * @property title - (Optional) A brief title or summary of the task item.
20
- * @property description - (Optional) A detailed description of the task item.
21
- * @property toolName - (Optional) The name of the tool being called (if applicable).
22
- * @property parameters - (Optional) A record of parameters associated with the tool call (if applicable).
23
- * @property error - (Optional) An error message if the task item represents an error.
24
- * @property result - (Optional) The result or output of the task item (if applicable).
25
- */
26
- export type AgentTaskMessageData = {
27
- type?: AgentTaskMessageDataType;
28
- title?: string;
29
- description?: string;
30
- toolName?: string;
31
- parameters?: Record<string, unknown>;
32
- error?: string;
33
- result?: string;
34
- };
35
- /**
36
- * Represents a single task item generated by the agent.
37
- * Each task item can be of different types such as tool calls, notifications, or errors.
38
- *
39
- * @property type - The type of the task item, defined by the AgentTaskItemType enum.
40
- * @property messageId - The unique identifier for the task item.
41
- * This is an incremental number representing the n-th task item.
42
- * @property data - The data associated with the task item, which varies based on the type.
43
- * @property isFinal - (Optional) Indicates if the task item is final and no further updates are expected.
44
- */
45
- export type AgentTaskMessage = {
46
- type: AgentTaskMessageType;
47
- messageId: number;
48
- data: AgentTaskMessageData;
49
- isFinal?: boolean;
50
- };
51
- export type AgentTaskMessageUpdate = {
52
- type: AgentTaskMessageType;
53
- messageId: number;
54
- updateType: "append";
55
- delta: AgentTaskMessageData;
56
- isFinal?: boolean;
57
- timeUsedSec?: string;
58
- };
@@ -1,5 +0,0 @@
1
- /**
2
- * An example function to echo the body of a POST request.
3
- * This route is accessible at /server-function/echo
4
- */
5
- export default function generate(req: Request): Promise<Response>;
@@ -1 +0,0 @@
1
- export default function handler(req: Request): Promise<Response>;
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "esnext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "declaration": true,
8
- "outDir": "dist",
9
- },
10
- "include": [
11
- "../../../../../../src/server-function/**/*",
12
- "../../../../../../pulse.config.ts",
13
- "../../../../../../global.d.ts",
14
- ],
15
- "exclude": [
16
- "../../../../../../node_modules",
17
- "../../../../../../dist",
18
- ]
19
- }
@@ -1,2 +0,0 @@
1
- import wp from 'webpack';
2
- export declare function createWebpackConfig(isPreview: boolean, buildTarget: 'client' | 'server' | 'both', mode: 'development' | 'production'): Promise<wp.Configuration[]>;
@@ -1,527 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
3
- import MiniCssExtractPlugin from 'mini-css-extract-plugin';
4
- import HtmlWebpackPlugin from 'html-webpack-plugin';
5
- import { networkInterfaces } from 'os';
6
- import path from 'path';
7
- import { globSync } from 'glob';
8
- import fs from 'fs';
9
- import CopyWebpackPlugin from 'copy-webpack-plugin';
10
- import ts from 'typescript';
11
- import { pathToFileURL } from 'url';
12
- import mfNode from '@module-federation/node';
13
- const { NodeFederationPlugin } = mfNode;
14
- import wp from 'webpack';
15
- const { webpack } = wp;
16
- export async function createWebpackConfig(isPreview, buildTarget, mode) {
17
- const projectDirName = process.cwd();
18
- async function loadPulseConfig() {
19
- // compile to js file and import
20
- const program = ts.createProgram({
21
- rootNames: [path.join(projectDirName, 'pulse.config.ts')],
22
- options: {
23
- module: ts.ModuleKind.ESNext,
24
- target: ts.ScriptTarget.ES2020,
25
- outDir: path.join(projectDirName, 'node_modules/.pulse/config'),
26
- esModuleInterop: true,
27
- skipLibCheck: true,
28
- forceConsistentCasingInFileNames: true,
29
- },
30
- });
31
- program.emit();
32
- // Fix imports in the generated js file for all files in node_modules/.pulse/config
33
- globSync('node_modules/.pulse/config/**/*.js', {
34
- cwd: projectDirName,
35
- absolute: true,
36
- }).forEach(jsFile => {
37
- let content = fs.readFileSync(jsFile, 'utf-8');
38
- content = content.replace(/(from\s+["']\.\/[^\s"']+)(["'])/g, (match, p1, p2) => {
39
- // No change if the import already has any extension
40
- if (p1.match(/\.(js|cjs|mjs|ts|tsx|json)$/)) {
41
- return match; // No change needed
42
- }
43
- return `${p1}.js${p2}`;
44
- });
45
- fs.writeFileSync(jsFile, content);
46
- });
47
- // Copy package.json if exists
48
- const pkgPath = path.join(projectDirName, 'package.json');
49
- if (fs.existsSync(pkgPath)) {
50
- const destPath = path.join(projectDirName, 'node_modules/.pulse/config/package.json');
51
- fs.copyFileSync(pkgPath, destPath);
52
- }
53
- const compiledConfig = path.join(projectDirName, 'node_modules/.pulse/config/pulse.config.js');
54
- const mod = await import(pathToFileURL(compiledConfig).href);
55
- // delete the compiled config after importing
56
- fs.rmSync(path.join(projectDirName, 'node_modules/.pulse/config'), {
57
- recursive: true,
58
- force: true,
59
- });
60
- return mod.default;
61
- }
62
- const pulseConfig = await loadPulseConfig();
63
- function getLocalNetworkIP() {
64
- const interfaces = networkInterfaces();
65
- for (const iface of Object.values(interfaces)) {
66
- if (!iface)
67
- continue;
68
- for (const config of iface) {
69
- if (config.family === 'IPv4' && !config.internal) {
70
- return config.address; // Returns the first non-internal IPv4 address
71
- }
72
- }
73
- }
74
- return 'localhost'; // Fallback
75
- }
76
- const origin = getLocalNetworkIP();
77
- const previewStartupMessage = `
78
- 🎉 Your Pulse extension preview \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
79
-
80
- ⚡️ Local: http://localhost:3030
81
- ⚡️ Network: http://${origin}:3030
82
-
83
- ✨ Try it out in your browser and let the magic happen! 🚀
84
- `;
85
- const devStartupMessage = `
86
- 🎉 Your Pulse extension \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
87
-
88
- ⚡️ Local: http://localhost:3030/${pulseConfig.id}/${pulseConfig.version}/
89
- ⚡️ Network: http://${origin}:3030/${pulseConfig.id}/${pulseConfig.version}/
90
-
91
- ✨ Try it out in the Pulse Editor and let the magic happen! 🚀
92
- `;
93
- // #region Node Federation Plugin for Server Functions
94
- function makeNodeFederationPlugin() {
95
- function discoverServerFunctions() {
96
- // Get all .ts files under src/server-function and read use default exports as entry points
97
- const files = globSync('./src/server-function/**/*.ts');
98
- const entryPoints = files
99
- .map(file => file.replaceAll('\\', '/'))
100
- .map(file => {
101
- return {
102
- ['./' +
103
- file.replace('src/server-function/', '').replace(/\.ts$/, '')]: './' + file,
104
- };
105
- })
106
- .reduce((acc, curr) => {
107
- return { ...acc, ...curr };
108
- }, {});
109
- return entryPoints;
110
- }
111
- const funcs = discoverServerFunctions();
112
- console.log(`Discovered server functions:
113
- ${Object.entries(funcs)
114
- .map(([name, file]) => {
115
- return ` - ${name.slice(2)} (from ${file})`;
116
- })
117
- .join('\n')}
118
- `);
119
- return new NodeFederationPlugin({
120
- name: pulseConfig.id + '_server',
121
- remoteType: 'script',
122
- useRuntimePlugin: true,
123
- library: { type: 'commonjs-module' },
124
- filename: 'remoteEntry.js',
125
- exposes: {
126
- ...funcs,
127
- },
128
- }, {});
129
- }
130
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
- function compileServerFunctions(compiler) {
132
- // Remove existing entry points
133
- try {
134
- fs.rmSync('dist/server', { recursive: true, force: true });
135
- }
136
- catch (e) {
137
- console.error('Error removing dist/server:', e);
138
- console.log('Continuing...');
139
- }
140
- // Generate tsconfig for server functions
141
- function generateTempTsConfig() {
142
- const tempTsConfigPath = path.join(process.cwd(), 'node_modules/.pulse/tsconfig.server.json');
143
- const tsConfig = {
144
- compilerOptions: {
145
- target: 'ES2020',
146
- module: 'esnext',
147
- moduleResolution: 'bundler',
148
- strict: true,
149
- declaration: true,
150
- outDir: path.join(process.cwd(), 'dist'),
151
- },
152
- include: [
153
- path.join(process.cwd(), 'src/server-function/**/*'),
154
- path.join(process.cwd(), 'pulse.config.ts'),
155
- path.join(process.cwd(), 'global.d.ts'),
156
- ],
157
- exclude: [
158
- path.join(process.cwd(), 'node_modules'),
159
- path.join(process.cwd(), 'dist'),
160
- ],
161
- };
162
- fs.writeFileSync(tempTsConfigPath, JSON.stringify(tsConfig, null, 2));
163
- }
164
- generateTempTsConfig();
165
- // Run a new webpack compilation to pick up new server functions
166
- const options = {
167
- ...compiler.options,
168
- watch: false,
169
- plugins: [
170
- // Add a new NodeFederationPlugin with updated entry points
171
- makeNodeFederationPlugin(),
172
- ],
173
- };
174
- const newCompiler = webpack(options);
175
- // Run the new compiler
176
- newCompiler?.run((err, stats) => {
177
- if (err) {
178
- console.error(`[Server] ❌ Error during recompilation:`, err);
179
- }
180
- else if (stats?.hasErrors()) {
181
- console.error(`[Server] ❌ Compilation errors:`, stats.toJson().errors);
182
- }
183
- else {
184
- console.log(`[Server] ✅ Compiled server functions successfully.`);
185
- }
186
- });
187
- }
188
- // #endregion
189
- // #region Source file parser for Pulse Config plugin
190
- class PulseConfigPlugin {
191
- requireFS = false;
192
- apply(compiler) {
193
- compiler.hooks.beforeCompile.tap('PulseConfigPlugin', () => {
194
- this.requireFS = false;
195
- globSync(['src/**/*.tsx', 'src/**/*.ts']).forEach(file => {
196
- const source = fs.readFileSync(file, 'utf8');
197
- this.scanSource(source);
198
- });
199
- // Persist result
200
- pulseConfig.requireWorkspace = this.requireFS;
201
- });
202
- }
203
- isWorkspaceHook(node) {
204
- return (ts.isCallExpression(node) &&
205
- ts.isIdentifier(node.expression) &&
206
- [
207
- 'useFileSystem',
208
- 'useFile',
209
- 'useReceiveFile',
210
- 'useTerminal',
211
- 'useWorkspaceInfo',
212
- ].includes(node.expression.text));
213
- }
214
- scanSource(sourceText) {
215
- const sourceFile = ts.createSourceFile('temp.tsx', sourceText, ts.ScriptTarget.Latest, true);
216
- const visit = (node) => {
217
- // Detect: useFileSystem(...)
218
- if (this.isWorkspaceHook(node)) {
219
- this.requireFS = true;
220
- }
221
- ts.forEachChild(node, visit);
222
- };
223
- visit(sourceFile);
224
- }
225
- }
226
- // #endregion
227
- // #region Webpack Configs
228
- const previewClientConfig = {
229
- mode: mode,
230
- entry: {
231
- main: './node_modules/.pulse/server/preview/frontend/index.js',
232
- },
233
- output: {
234
- path: path.resolve(projectDirName, 'dist/client'),
235
- },
236
- resolve: {
237
- extensions: ['.ts', '.tsx', '.js'],
238
- },
239
- plugins: [
240
- new PulseConfigPlugin(),
241
- new HtmlWebpackPlugin({
242
- template: './node_modules/.pulse/server/preview/frontend/index.html',
243
- }),
244
- new MiniCssExtractPlugin({
245
- filename: 'globals.css',
246
- }),
247
- new CopyWebpackPlugin({
248
- patterns: [{ from: 'src/assets', to: 'assets' }],
249
- }),
250
- {
251
- apply: compiler => {
252
- let isFirstRun = true;
253
- // Before build starts
254
- compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
255
- if (!isFirstRun) {
256
- console.log('[client-preview] 🔄 Reloading app...');
257
- }
258
- else {
259
- console.log('[client-preview] 🔄 Building app...');
260
- }
261
- });
262
- // After build finishes
263
- compiler.hooks.done.tap('ReloadMessagePlugin', () => {
264
- if (isFirstRun) {
265
- console.log('[client-preview] ✅ Successfully built preview.');
266
- console.log(previewStartupMessage);
267
- isFirstRun = false;
268
- }
269
- else {
270
- console.log('[client-preview] ✅ Reload finished');
271
- }
272
- // Write pulse config to dist
273
- fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
274
- fs.writeFileSync(path.resolve(projectDirName, 'dist/server/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
275
- });
276
- },
277
- },
278
- ],
279
- watchOptions: {
280
- ignored: /src\/server-function/,
281
- },
282
- module: {
283
- rules: [
284
- {
285
- test: /\.tsx?$/,
286
- use: 'ts-loader',
287
- exclude: [/node_modules/, /dist/],
288
- },
289
- {
290
- test: /\.css$/i,
291
- use: [
292
- MiniCssExtractPlugin.loader,
293
- 'css-loader',
294
- {
295
- loader: 'postcss-loader',
296
- },
297
- ],
298
- },
299
- ],
300
- },
301
- stats: {
302
- all: false,
303
- errors: true,
304
- warnings: true,
305
- logging: 'warn',
306
- colors: true,
307
- },
308
- infrastructureLogging: {
309
- level: 'warn',
310
- },
311
- };
312
- const mfClientConfig = {
313
- mode: mode,
314
- name: 'client',
315
- entry: './src/main.tsx',
316
- output: {
317
- publicPath: 'auto',
318
- path: path.resolve(projectDirName, 'dist/client'),
319
- },
320
- resolve: {
321
- extensions: ['.ts', '.tsx', '.js'],
322
- },
323
- plugins: [
324
- new PulseConfigPlugin(),
325
- new MiniCssExtractPlugin({
326
- filename: 'globals.css',
327
- }),
328
- // Copy assets to dist
329
- new CopyWebpackPlugin({
330
- patterns: [{ from: 'src/assets', to: 'assets' }],
331
- }),
332
- new ModuleFederationPlugin({
333
- // Do not use hyphen character '-' in the name
334
- name: pulseConfig.id,
335
- filename: 'remoteEntry.js',
336
- exposes: {
337
- './main': './src/main.tsx',
338
- },
339
- shared: {
340
- react: {
341
- requiredVersion: '19.2.0',
342
- import: 'react', // the "react" package will be used a provided and fallback module
343
- shareKey: 'react', // under this name the shared module will be placed in the share scope
344
- shareScope: 'default', // share scope with this name will be used
345
- singleton: true, // only a single version of the shared module is allowed
346
- },
347
- 'react-dom': {
348
- requiredVersion: '19.2.0',
349
- singleton: true, // only a single version of the shared module is allowed
350
- },
351
- },
352
- }),
353
- {
354
- apply: compiler => {
355
- if (compiler.options.mode === 'development') {
356
- let isFirstRun = true;
357
- // Before build starts
358
- compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
359
- if (!isFirstRun) {
360
- console.log('[client] 🔄 reloading app...');
361
- }
362
- else {
363
- console.log('[client] 🔄 building app...');
364
- }
365
- });
366
- // Log file updates
367
- compiler.hooks.invalid.tap('LogFileUpdates', (file, changeTime) => {
368
- console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
369
- });
370
- // After build finishes
371
- compiler.hooks.done.tap('ReloadMessagePlugin', () => {
372
- if (isFirstRun) {
373
- console.log('[client] ✅ Successfully built client.');
374
- console.log(devStartupMessage);
375
- isFirstRun = false;
376
- }
377
- else {
378
- console.log('[client] ✅ Reload finished.');
379
- }
380
- // Write pulse config to dist
381
- fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
382
- });
383
- }
384
- else {
385
- // Print build success/failed message
386
- compiler.hooks.done.tap('BuildMessagePlugin', stats => {
387
- if (stats.hasErrors()) {
388
- console.log(`[client] ❌ Failed to build client.`);
389
- }
390
- else {
391
- console.log(`[client] ✅ Successfully built client.`);
392
- // Write pulse config to dist
393
- fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
394
- }
395
- });
396
- }
397
- },
398
- },
399
- ],
400
- module: {
401
- rules: [
402
- {
403
- test: /\.tsx?$/,
404
- use: 'ts-loader',
405
- exclude: [/node_modules/, /dist/],
406
- },
407
- {
408
- test: /\.css$/i,
409
- use: [
410
- MiniCssExtractPlugin.loader,
411
- 'css-loader',
412
- {
413
- loader: 'postcss-loader',
414
- },
415
- ],
416
- exclude: [/dist/],
417
- },
418
- ],
419
- },
420
- stats: {
421
- all: false,
422
- errors: true,
423
- warnings: true,
424
- logging: 'warn',
425
- colors: true,
426
- assets: false,
427
- },
428
- infrastructureLogging: {
429
- level: 'warn',
430
- },
431
- };
432
- const mfServerConfig = {
433
- mode: mode,
434
- name: 'server',
435
- entry: {},
436
- target: 'async-node',
437
- output: {
438
- publicPath: 'auto',
439
- path: path.resolve(projectDirName, 'dist/server'),
440
- },
441
- resolve: {
442
- extensions: ['.ts', '.js'],
443
- },
444
- plugins: [
445
- {
446
- apply: compiler => {
447
- if (compiler.options.mode === 'development') {
448
- let isFirstRun = true;
449
- // Before build starts
450
- compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
451
- if (!isFirstRun) {
452
- console.log(`[Server] 🔄 Reloading app...`);
453
- }
454
- else {
455
- console.log(`[Server] 🔄 Building app...`);
456
- }
457
- compileServerFunctions(compiler);
458
- });
459
- // After build finishes
460
- compiler.hooks.done.tap('ReloadMessagePlugin', () => {
461
- if (isFirstRun) {
462
- console.log(`[Server] ✅ Successfully built server.`);
463
- isFirstRun = false;
464
- }
465
- else {
466
- console.log(`[Server] ✅ Reload finished.`);
467
- }
468
- });
469
- // Watch for changes in the server-function directory to trigger rebuilds
470
- compiler.hooks.thisCompilation.tap('WatchServerFunctions', compilation => {
471
- compilation.contextDependencies.add(path.resolve(projectDirName, 'src/server-function'));
472
- });
473
- }
474
- else {
475
- // Print build success/failed message
476
- compiler.hooks.done.tap('BuildMessagePlugin', stats => {
477
- if (stats.hasErrors()) {
478
- console.log(`[Server] ❌ Failed to build server.`);
479
- }
480
- else {
481
- compileServerFunctions(compiler);
482
- console.log(`[Server] ✅ Successfully built server.`);
483
- }
484
- });
485
- }
486
- },
487
- },
488
- ],
489
- module: {
490
- rules: [
491
- {
492
- test: /\.tsx?$/,
493
- use: {
494
- loader: 'ts-loader',
495
- options: {
496
- configFile: 'node_modules/.pulse/tsconfig.server.json',
497
- },
498
- },
499
- exclude: [/node_modules/, /dist/],
500
- },
501
- ],
502
- },
503
- stats: {
504
- all: false,
505
- errors: true,
506
- warnings: true,
507
- logging: 'warn',
508
- colors: true,
509
- },
510
- infrastructureLogging: {
511
- level: 'warn',
512
- },
513
- };
514
- // #endregion
515
- if (isPreview) {
516
- return [previewClientConfig, mfServerConfig];
517
- }
518
- else if (buildTarget === 'server') {
519
- return [mfServerConfig];
520
- }
521
- else if (buildTarget === 'client') {
522
- return [mfClientConfig];
523
- }
524
- else {
525
- return [mfClientConfig, mfServerConfig];
526
- }
527
- }