@pulse-editor/cli 0.1.1-beta.33 → 0.1.1-beta.36
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/lib/backend/publish-app.js +1 -1
- package/dist/lib/server/express.js +32 -32
- package/dist/lib/server/utils.js +3 -3
- package/dist/lib/webpack/compile.js +1 -1
- package/dist/lib/webpack/configs/mf-client.d.ts +3 -0
- package/dist/lib/webpack/configs/mf-client.js +183 -0
- package/dist/lib/webpack/configs/mf-server.d.ts +2 -0
- package/dist/lib/webpack/configs/mf-server.js +391 -0
- package/dist/lib/webpack/configs/preview.d.ts +3 -0
- package/dist/lib/webpack/configs/preview.js +110 -0
- package/dist/lib/webpack/configs/utils.d.ts +6 -0
- package/dist/lib/webpack/configs/utils.js +118 -0
- package/dist/lib/webpack/webpack-config.d.ts +1 -0
- package/dist/lib/webpack/webpack-config.js +24 -0
- package/package.json +4 -6
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
export async function publishApp(isStage) {
|
|
4
4
|
// Upload the zip file to the server
|
|
5
5
|
// Read pulse.config.json for visibility
|
|
6
|
-
const config = JSON.parse(fs.readFileSync('./dist/
|
|
6
|
+
const config = JSON.parse(fs.readFileSync('./dist/pulse.config.json', 'utf-8'));
|
|
7
7
|
const visibility = config.visibility;
|
|
8
8
|
const formData = new FormData();
|
|
9
9
|
const buffer = fs.readFileSync('./node_modules/@pulse-editor/dist.zip');
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This is a local dev server for "npm run dev" and "npm run preview".
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
import cors from
|
|
6
|
-
import dotenv from
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { networkInterfaces } from
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
4
|
+
import connectLivereload from "connect-livereload";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
import express from "express";
|
|
8
|
+
import livereload from "livereload";
|
|
9
|
+
import { networkInterfaces } from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { pipeline, Readable } from "stream";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
13
|
+
import { promisify } from "util";
|
|
14
|
+
import { readConfigFile } from "../webpack/configs/utils.js";
|
|
15
15
|
dotenv.config({
|
|
16
16
|
quiet: true,
|
|
17
17
|
});
|
|
18
|
-
const isPreview = process.env?.[
|
|
19
|
-
const isDev = process.env?.[
|
|
20
|
-
const workspaceId = process.env?.[
|
|
18
|
+
const isPreview = process.env?.["PREVIEW"];
|
|
19
|
+
const isDev = process.env?.["NODE_ENV"] === "development";
|
|
20
|
+
const workspaceId = process.env?.["WORKSPACE_ID"];
|
|
21
21
|
const pulseConfig = await readConfigFile();
|
|
22
22
|
if (isDev || isPreview) {
|
|
23
23
|
const livereloadServer = livereload.createServer({
|
|
24
24
|
// @ts-expect-error override server options
|
|
25
|
-
host:
|
|
25
|
+
host: "0.0.0.0",
|
|
26
26
|
});
|
|
27
|
-
livereloadServer.watch(
|
|
28
|
-
livereloadServer.server.once(
|
|
29
|
-
console.log(
|
|
27
|
+
livereloadServer.watch("dist");
|
|
28
|
+
livereloadServer.server.once("connection", () => {
|
|
29
|
+
console.log("✅ LiveReload connected");
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
const app = express();
|
|
@@ -49,24 +49,24 @@ app.use((req, res, next) => {
|
|
|
49
49
|
return next();
|
|
50
50
|
});
|
|
51
51
|
// Serve backend
|
|
52
|
-
app.use(`/${pulseConfig.id}/${pulseConfig.version}/server`, express.static(
|
|
52
|
+
app.use(`/${pulseConfig.id}/${pulseConfig.version}/server`, express.static("dist/server"));
|
|
53
53
|
// Catch backend function calls
|
|
54
54
|
app.all(/^\/server-function\/(.*)/, async (req, res) => {
|
|
55
55
|
const func = req.params[0];
|
|
56
|
-
const url = `${req.protocol}://${req.get(
|
|
56
|
+
const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
57
57
|
// Convert Express req -> Fetch Request
|
|
58
58
|
const request = new Request(url, {
|
|
59
59
|
method: req.method,
|
|
60
60
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
61
|
headers: req.headers,
|
|
62
|
-
body: [
|
|
62
|
+
body: ["GET", "HEAD"].includes(req.method)
|
|
63
63
|
? null
|
|
64
64
|
: JSON.stringify(req.body),
|
|
65
65
|
});
|
|
66
|
-
const dir = path.resolve(
|
|
66
|
+
const dir = path.resolve("node_modules/@pulse-editor/cli/dist/lib/server/preview/backend/load-remote.cjs");
|
|
67
67
|
const fileUrl = pathToFileURL(dir).href;
|
|
68
68
|
const { loadFunc, loadPrice } = await import(fileUrl);
|
|
69
|
-
const price = await loadPrice(func, pulseConfig.id,
|
|
69
|
+
const price = await loadPrice(func, pulseConfig.id, "http://localhost:3030", pulseConfig.version);
|
|
70
70
|
if (price) {
|
|
71
71
|
// Make func name and price bold in console
|
|
72
72
|
console.log(`🏃 Running function \x1b[1m${func}\x1b[0m, credits consumed: \x1b[1m${price}\x1b[0m`);
|
|
@@ -75,7 +75,7 @@ app.all(/^\/server-function\/(.*)/, async (req, res) => {
|
|
|
75
75
|
console.log(`🏃 Running function \x1b[1m${func}\x1b[0m.`);
|
|
76
76
|
}
|
|
77
77
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
-
const loadedFunc = await loadFunc(func, pulseConfig.id,
|
|
78
|
+
const loadedFunc = await loadFunc(func, pulseConfig.id, "http://localhost:3030", pulseConfig.version);
|
|
79
79
|
const response = await loadedFunc(request);
|
|
80
80
|
const streamPipeline = promisify(pipeline);
|
|
81
81
|
if (response) {
|
|
@@ -97,18 +97,18 @@ app.all(/^\/server-function\/(.*)/, async (req, res) => {
|
|
|
97
97
|
});
|
|
98
98
|
if (isPreview) {
|
|
99
99
|
/* Preview mode */
|
|
100
|
-
app.use(express.static(
|
|
101
|
-
app.listen(3030,
|
|
100
|
+
app.use(express.static("dist/client"));
|
|
101
|
+
app.listen(3030, "0.0.0.0");
|
|
102
102
|
}
|
|
103
103
|
else if (isDev) {
|
|
104
104
|
/* Dev mode */
|
|
105
|
-
app.use(`/${pulseConfig.id}/${pulseConfig.version}`, express.static(
|
|
106
|
-
app.listen(3030,
|
|
105
|
+
app.use(`/${pulseConfig.id}/${pulseConfig.version}`, express.static("dist"));
|
|
106
|
+
app.listen(3030, "0.0.0.0");
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
109
|
/* Production mode */
|
|
110
|
-
app.use(`/${pulseConfig.id}/${pulseConfig.version}`, express.static(
|
|
111
|
-
app.listen(3030,
|
|
110
|
+
app.use(`/${pulseConfig.id}/${pulseConfig.version}`, express.static("dist"));
|
|
111
|
+
app.listen(3030, "0.0.0.0", () => {
|
|
112
112
|
console.log(`\
|
|
113
113
|
🎉 Your Pulse extension \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
|
|
114
114
|
|
|
@@ -124,10 +124,10 @@ function getLocalNetworkIP() {
|
|
|
124
124
|
if (!iface)
|
|
125
125
|
continue;
|
|
126
126
|
for (const config of iface) {
|
|
127
|
-
if (config.family ===
|
|
127
|
+
if (config.family === "IPv4" && !config.internal) {
|
|
128
128
|
return config.address; // Returns the first non-internal IPv4 address
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
return
|
|
132
|
+
return "localhost"; // Fallback
|
|
133
133
|
}
|
package/dist/lib/server/utils.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
export async function readConfigFile() {
|
|
3
3
|
// Read pulse.config.json from dist/client
|
|
4
|
-
// Wait until dist/
|
|
4
|
+
// Wait until dist/pulse.config.json exists
|
|
5
5
|
while (true) {
|
|
6
6
|
try {
|
|
7
|
-
await fs.access('dist/
|
|
7
|
+
await fs.access('dist/pulse.config.json');
|
|
8
8
|
break;
|
|
9
9
|
}
|
|
10
10
|
catch (err) {
|
|
@@ -12,6 +12,6 @@ export async function readConfigFile() {
|
|
|
12
12
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
const data = await fs.readFile('dist/
|
|
15
|
+
const data = await fs.readFile('dist/pulse.config.json', 'utf-8');
|
|
16
16
|
return JSON.parse(data);
|
|
17
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import webpack from 'webpack';
|
|
2
|
-
import { createWebpackConfig } from '
|
|
2
|
+
import { createWebpackConfig } from './webpack-config.js';
|
|
3
3
|
export async function webpackCompile(mode, buildTarget, isWatchMode = false) {
|
|
4
4
|
const configs = await createWebpackConfig(mode === 'preview', buildTarget ?? 'both', mode === 'development'
|
|
5
5
|
? 'development'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ModuleFederationPlugin } from "@module-federation/enhanced/webpack";
|
|
3
|
+
import CopyWebpackPlugin from "copy-webpack-plugin";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { globSync } from "glob";
|
|
6
|
+
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import { discoverAppSkillActions, getLocalNetworkIP, loadPulseConfig, } from "./utils.js";
|
|
10
|
+
class MFClientPlugin {
|
|
11
|
+
projectDirName;
|
|
12
|
+
pulseConfig;
|
|
13
|
+
origin;
|
|
14
|
+
constructor(pulseConfig) {
|
|
15
|
+
this.projectDirName = process.cwd();
|
|
16
|
+
this.pulseConfig = pulseConfig;
|
|
17
|
+
this.origin = getLocalNetworkIP();
|
|
18
|
+
}
|
|
19
|
+
apply(compiler) {
|
|
20
|
+
if (compiler.options.mode === "development") {
|
|
21
|
+
let isFirstRun = true;
|
|
22
|
+
// Before build starts
|
|
23
|
+
compiler.hooks.watchRun.tap("ReloadMessagePlugin", () => {
|
|
24
|
+
if (!isFirstRun) {
|
|
25
|
+
console.log("[client] 🔄 reloading app...");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log("[client] 🔄 building app...");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Log file updates
|
|
32
|
+
compiler.hooks.invalid.tap("LogFileUpdates", (file, changeTime) => {
|
|
33
|
+
console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
|
|
34
|
+
});
|
|
35
|
+
const devStartupMessage = `
|
|
36
|
+
🎉 Your Pulse extension \x1b[1m${this.pulseConfig.displayName}\x1b[0m is LIVE!
|
|
37
|
+
|
|
38
|
+
⚡️ Local: http://localhost:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
39
|
+
⚡️ Network: http://${this.origin}:3030/${this.pulseConfig.id}/${this.pulseConfig.version}/
|
|
40
|
+
|
|
41
|
+
✨ Try it out in the Pulse Editor and let the magic happen! 🚀
|
|
42
|
+
`;
|
|
43
|
+
// After build finishes
|
|
44
|
+
compiler.hooks.done.tap("ReloadMessagePlugin", () => {
|
|
45
|
+
if (isFirstRun) {
|
|
46
|
+
console.log("[client] ✅ Successfully built client.");
|
|
47
|
+
console.log(devStartupMessage);
|
|
48
|
+
isFirstRun = false;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log("[client] ✅ Reload finished.");
|
|
52
|
+
}
|
|
53
|
+
// Write pulse config to dist
|
|
54
|
+
fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Print build success/failed message
|
|
59
|
+
compiler.hooks.done.tap("BuildMessagePlugin", (stats) => {
|
|
60
|
+
if (stats.hasErrors()) {
|
|
61
|
+
console.log(`[client] ❌ Failed to build client.`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(`[client] ✅ Successfully built client.`);
|
|
65
|
+
// Write pulse config to dist
|
|
66
|
+
fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
compiler.hooks.beforeCompile.tap("PulseConfigPlugin", () => {
|
|
71
|
+
let requireFS = false;
|
|
72
|
+
function isWorkspaceHook(node) {
|
|
73
|
+
return (ts.isCallExpression(node) &&
|
|
74
|
+
ts.isIdentifier(node.expression) &&
|
|
75
|
+
[
|
|
76
|
+
"useFileSystem",
|
|
77
|
+
"useFile",
|
|
78
|
+
"useReceiveFile",
|
|
79
|
+
"useTerminal",
|
|
80
|
+
"useWorkspaceInfo",
|
|
81
|
+
].includes(node.expression.text));
|
|
82
|
+
}
|
|
83
|
+
function scanSource(sourceText) {
|
|
84
|
+
const sourceFile = ts.createSourceFile("temp.tsx", sourceText, ts.ScriptTarget.Latest, true);
|
|
85
|
+
const visit = (node) => {
|
|
86
|
+
// Detect: useFileSystem(...)
|
|
87
|
+
if (isWorkspaceHook(node)) {
|
|
88
|
+
requireFS = true;
|
|
89
|
+
}
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
};
|
|
92
|
+
visit(sourceFile);
|
|
93
|
+
}
|
|
94
|
+
globSync(["src/**/*.tsx", "src/**/*.ts"]).forEach((file) => {
|
|
95
|
+
const source = fs.readFileSync(file, "utf8");
|
|
96
|
+
scanSource(source);
|
|
97
|
+
});
|
|
98
|
+
// Persist result
|
|
99
|
+
this.pulseConfig.requireWorkspace = requireFS;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function makeMFClientConfig(mode) {
|
|
104
|
+
const projectDirName = process.cwd();
|
|
105
|
+
const pulseConfig = await loadPulseConfig();
|
|
106
|
+
const mainComponent = "./src/main.tsx";
|
|
107
|
+
const actions = discoverAppSkillActions();
|
|
108
|
+
return {
|
|
109
|
+
mode: mode,
|
|
110
|
+
name: "client",
|
|
111
|
+
entry: mainComponent,
|
|
112
|
+
output: {
|
|
113
|
+
publicPath: "auto",
|
|
114
|
+
path: path.resolve(projectDirName, "dist/client"),
|
|
115
|
+
},
|
|
116
|
+
resolve: {
|
|
117
|
+
extensions: [".ts", ".tsx", ".js"],
|
|
118
|
+
},
|
|
119
|
+
plugins: [
|
|
120
|
+
new MiniCssExtractPlugin({
|
|
121
|
+
filename: "globals.css",
|
|
122
|
+
}),
|
|
123
|
+
// Copy assets to dist
|
|
124
|
+
new CopyWebpackPlugin({
|
|
125
|
+
patterns: [{ from: "src/assets", to: "assets" }],
|
|
126
|
+
}),
|
|
127
|
+
new ModuleFederationPlugin({
|
|
128
|
+
// Do not use hyphen character '-' in the name
|
|
129
|
+
name: pulseConfig.id + "_client",
|
|
130
|
+
filename: "remoteEntry.js",
|
|
131
|
+
exposes: {
|
|
132
|
+
"./main": mainComponent,
|
|
133
|
+
...actions,
|
|
134
|
+
},
|
|
135
|
+
shared: {
|
|
136
|
+
react: {
|
|
137
|
+
requiredVersion: "19.2.0",
|
|
138
|
+
import: "react", // the "react" package will be used a provided and fallback module
|
|
139
|
+
shareKey: "react", // under this name the shared module will be placed in the share scope
|
|
140
|
+
shareScope: "default", // share scope with this name will be used
|
|
141
|
+
singleton: true, // only a single version of the shared module is allowed
|
|
142
|
+
},
|
|
143
|
+
"react-dom": {
|
|
144
|
+
requiredVersion: "19.2.0",
|
|
145
|
+
singleton: true, // only a single version of the shared module is allowed
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
new MFClientPlugin(pulseConfig),
|
|
150
|
+
],
|
|
151
|
+
module: {
|
|
152
|
+
rules: [
|
|
153
|
+
{
|
|
154
|
+
test: /\.tsx?$/,
|
|
155
|
+
use: "ts-loader",
|
|
156
|
+
exclude: [/node_modules/, /dist/],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
test: /\.css$/i,
|
|
160
|
+
use: [
|
|
161
|
+
MiniCssExtractPlugin.loader,
|
|
162
|
+
"css-loader",
|
|
163
|
+
{
|
|
164
|
+
loader: "postcss-loader",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
exclude: [/dist/],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
stats: {
|
|
172
|
+
all: false,
|
|
173
|
+
errors: true,
|
|
174
|
+
warnings: true,
|
|
175
|
+
logging: "warn",
|
|
176
|
+
colors: true,
|
|
177
|
+
assets: false,
|
|
178
|
+
},
|
|
179
|
+
infrastructureLogging: {
|
|
180
|
+
level: "warn",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import mfNode from "@module-federation/node";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { globSync } from "glob";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
7
|
+
import wp from "webpack";
|
|
8
|
+
import { discoverAppSkillActions, loadPulseConfig } from "./utils.js";
|
|
9
|
+
const { NodeFederationPlugin } = mfNode;
|
|
10
|
+
const { webpack } = wp;
|
|
11
|
+
class MFServerPlugin {
|
|
12
|
+
projectDirName;
|
|
13
|
+
pulseConfig;
|
|
14
|
+
constructor(pulseConfig) {
|
|
15
|
+
this.projectDirName = process.cwd();
|
|
16
|
+
this.pulseConfig = pulseConfig;
|
|
17
|
+
}
|
|
18
|
+
apply(compiler) {
|
|
19
|
+
if (compiler.options.mode === "development") {
|
|
20
|
+
let isFirstRun = true;
|
|
21
|
+
// Before build starts
|
|
22
|
+
compiler.hooks.watchRun.tap("ReloadMessagePlugin", async (compilation) => {
|
|
23
|
+
this.cleanServerDist();
|
|
24
|
+
if (!isFirstRun) {
|
|
25
|
+
console.log(`[Server] 🔄 Reloading app...`);
|
|
26
|
+
const isServerFunctionChange = compilation.modifiedFiles
|
|
27
|
+
? Array.from(compilation.modifiedFiles).some((file) => file.includes("src/server-function"))
|
|
28
|
+
: false;
|
|
29
|
+
if (isServerFunctionChange) {
|
|
30
|
+
await this.compileServerFunctions(compiler);
|
|
31
|
+
}
|
|
32
|
+
const isActionChange = compilation.modifiedFiles
|
|
33
|
+
? Array.from(compilation.modifiedFiles).some((file) => file.includes("src/action"))
|
|
34
|
+
: false;
|
|
35
|
+
if (isActionChange) {
|
|
36
|
+
console.log(`[Server] Detected changes in actions. Recompiling...`);
|
|
37
|
+
this.compileAppActionSkills();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(`[Server] 🔄 Building app...`);
|
|
42
|
+
await this.compileServerFunctions(compiler);
|
|
43
|
+
this.compileAppActionSkills();
|
|
44
|
+
console.log(`[Server] ✅ Successfully built server.`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// After build finishes
|
|
48
|
+
compiler.hooks.done.tap("ReloadMessagePlugin", () => {
|
|
49
|
+
if (isFirstRun) {
|
|
50
|
+
isFirstRun = false;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(`[Server] ✅ Reload finished.`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Watch for file changes in the server-function directory to trigger server-function rebuilds
|
|
57
|
+
compiler.hooks.thisCompilation.tap("WatchServerFunctions", (compilation) => {
|
|
58
|
+
compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/server-function"));
|
|
59
|
+
});
|
|
60
|
+
// Watch for file changes in the action directory to trigger action rebuilds
|
|
61
|
+
compiler.hooks.thisCompilation.tap("WatchActions", (compilation) => {
|
|
62
|
+
compilation.contextDependencies.add(path.resolve(this.projectDirName, "src/action"));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Print build success/failed message
|
|
67
|
+
compiler.hooks.done.tap("BuildMessagePlugin", async (stats) => {
|
|
68
|
+
if (stats.hasErrors()) {
|
|
69
|
+
console.log(`[Server] ❌ Failed to build server.`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
try {
|
|
73
|
+
await this.compileServerFunctions(compiler);
|
|
74
|
+
this.compileAppActionSkills();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.log(`[Server] ❌ Error during compilation:`, err);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(`[Server] ✅ Successfully built server.`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cleanServerDist() {
|
|
86
|
+
// Remove existing entry points
|
|
87
|
+
try {
|
|
88
|
+
fs.rmSync("dist/server", { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.error("Error removing dist/server:", e);
|
|
92
|
+
console.log("Continuing...");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Programmatically call webpack to compile server functions
|
|
97
|
+
* whenever there are changes in the src/server-function directory.
|
|
98
|
+
* This is necessary because Module Federation needs to know about
|
|
99
|
+
* all the exposed modules at build time, so we have to trigger a
|
|
100
|
+
* new compilation whenever server functions are added/removed/changed.
|
|
101
|
+
* @param compiler
|
|
102
|
+
*/
|
|
103
|
+
async compileServerFunctions(compiler) {
|
|
104
|
+
// Generate tsconfig for server functions
|
|
105
|
+
function generateTempTsConfig() {
|
|
106
|
+
const tempTsConfigPath = path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json");
|
|
107
|
+
const tsConfig = {
|
|
108
|
+
compilerOptions: {
|
|
109
|
+
target: "ES2020",
|
|
110
|
+
module: "esnext",
|
|
111
|
+
moduleResolution: "bundler",
|
|
112
|
+
strict: true,
|
|
113
|
+
declaration: true,
|
|
114
|
+
outDir: path.join(process.cwd(), "dist"),
|
|
115
|
+
},
|
|
116
|
+
include: [
|
|
117
|
+
path.join(process.cwd(), "src/server-function/**/*"),
|
|
118
|
+
path.join(process.cwd(), "pulse.config.ts"),
|
|
119
|
+
path.join(process.cwd(), "global.d.ts"),
|
|
120
|
+
],
|
|
121
|
+
exclude: [
|
|
122
|
+
path.join(process.cwd(), "node_modules"),
|
|
123
|
+
path.join(process.cwd(), "dist"),
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
fs.writeFileSync(tempTsConfigPath, JSON.stringify(tsConfig, null, 2));
|
|
127
|
+
}
|
|
128
|
+
generateTempTsConfig();
|
|
129
|
+
// Run a new webpack compilation to pick up new server functions
|
|
130
|
+
const options = {
|
|
131
|
+
...compiler.options,
|
|
132
|
+
watch: false,
|
|
133
|
+
plugins: [
|
|
134
|
+
// Add a new NodeFederationPlugin with updated entry points
|
|
135
|
+
this.makeNodeFederationPlugin(),
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
const newCompiler = webpack(options);
|
|
139
|
+
// Run the new compiler
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
newCompiler?.run((err, stats) => {
|
|
142
|
+
if (err) {
|
|
143
|
+
console.error(`[Server] ❌ Error during recompilation:`, err);
|
|
144
|
+
reject(err);
|
|
145
|
+
}
|
|
146
|
+
else if (stats?.hasErrors()) {
|
|
147
|
+
console.error(`[Server] ❌ Compilation errors:`, stats.toJson().errors);
|
|
148
|
+
reject(new Error("Compilation errors"));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(`[Server] ✅ Compiled server functions successfully.`);
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
makeNodeFederationPlugin() {
|
|
158
|
+
function discoverServerFunctions() {
|
|
159
|
+
// Get all .ts files under src/server-function and read use default exports as entry points
|
|
160
|
+
const files = globSync("./src/server-function/**/*.ts");
|
|
161
|
+
const entryPoints = files
|
|
162
|
+
.map((file) => file.replaceAll("\\", "/"))
|
|
163
|
+
.map((file) => {
|
|
164
|
+
return {
|
|
165
|
+
["./" +
|
|
166
|
+
file.replace("src/server-function/", "").replace(/\.ts$/, "")]: "./" + file,
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
.reduce((acc, curr) => {
|
|
170
|
+
return { ...acc, ...curr };
|
|
171
|
+
}, {});
|
|
172
|
+
return entryPoints;
|
|
173
|
+
}
|
|
174
|
+
const funcs = discoverServerFunctions();
|
|
175
|
+
const actions = discoverAppSkillActions();
|
|
176
|
+
console.log(`Discovered server functions:
|
|
177
|
+
${Object.entries(funcs)
|
|
178
|
+
.map(([name, file]) => {
|
|
179
|
+
return ` - ${name.slice(2)} (from ${file})`;
|
|
180
|
+
})
|
|
181
|
+
.join("\n")}
|
|
182
|
+
`);
|
|
183
|
+
return new NodeFederationPlugin({
|
|
184
|
+
name: this.pulseConfig.id + "_server",
|
|
185
|
+
remoteType: "script",
|
|
186
|
+
useRuntimePlugin: true,
|
|
187
|
+
library: { type: "commonjs-module" },
|
|
188
|
+
filename: "remoteEntry.js",
|
|
189
|
+
exposes: {
|
|
190
|
+
...funcs,
|
|
191
|
+
...actions,
|
|
192
|
+
},
|
|
193
|
+
}, {});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Register default functions defined in src/action as exposed modules in Module Federation.
|
|
197
|
+
* This will:
|
|
198
|
+
* 1. Search for all .ts files under src/action
|
|
199
|
+
* 2. Use ts-morph to get the default function information, including function name, parameters, and JSDoc comments
|
|
200
|
+
* 3. Organize the functions' information into a list of Action
|
|
201
|
+
* @param compiler
|
|
202
|
+
*/
|
|
203
|
+
compileAppActionSkills() {
|
|
204
|
+
// 1. Get all TypeScript files under src/skill
|
|
205
|
+
const files = globSync("./src/skill/*/action.ts");
|
|
206
|
+
const project = new Project({
|
|
207
|
+
tsConfigFilePath: path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json"),
|
|
208
|
+
});
|
|
209
|
+
const actions = [];
|
|
210
|
+
files.forEach((file) => {
|
|
211
|
+
const sourceFile = project.addSourceFileAtPath(file);
|
|
212
|
+
const defaultExportSymbol = sourceFile.getDefaultExportSymbol();
|
|
213
|
+
if (!defaultExportSymbol)
|
|
214
|
+
return;
|
|
215
|
+
const defaultExportDeclarations = defaultExportSymbol.getDeclarations();
|
|
216
|
+
defaultExportDeclarations.forEach((declaration) => {
|
|
217
|
+
if (declaration.getKind() !== SyntaxKind.FunctionDeclaration)
|
|
218
|
+
return;
|
|
219
|
+
const funcDecl = declaration.asKindOrThrow(SyntaxKind.FunctionDeclaration);
|
|
220
|
+
// Extract JSDoc comments
|
|
221
|
+
const funcName = funcDecl.getName() ?? "default";
|
|
222
|
+
// Throw an error if the funcName is duplicated with an existing action to prevent accidental overwriting
|
|
223
|
+
if (actions.some((action) => action.name === funcName)) {
|
|
224
|
+
throw new Error(`Duplicate action name "${funcName}" detected in file ${file}. Please ensure all actions have unique names to avoid conflicts.`);
|
|
225
|
+
}
|
|
226
|
+
const defaultExportJSDocs = funcDecl.getJsDocs();
|
|
227
|
+
const description = defaultExportJSDocs
|
|
228
|
+
.map((doc) => doc.getFullText())
|
|
229
|
+
.join("\n");
|
|
230
|
+
const allJSDocs = sourceFile.getDescendantsOfKind(SyntaxKind.JSDoc);
|
|
231
|
+
const typeDefs = this.parseTypeDefs(allJSDocs);
|
|
232
|
+
/* Extract parameter descriptions from JSDoc */
|
|
233
|
+
const funcParam = funcDecl.getParameters()[0];
|
|
234
|
+
const params = {};
|
|
235
|
+
if (funcParam) {
|
|
236
|
+
/**
|
|
237
|
+
* Extract default values from the destructured parameter
|
|
238
|
+
* (ObjectBindingPattern → BindingElement initializer)
|
|
239
|
+
*/
|
|
240
|
+
const defaults = new Map();
|
|
241
|
+
const nameNode = funcParam.getNameNode();
|
|
242
|
+
if (Node.isObjectBindingPattern(nameNode)) {
|
|
243
|
+
nameNode.getElements().forEach((el) => {
|
|
244
|
+
if (!Node.isBindingElement(el))
|
|
245
|
+
return;
|
|
246
|
+
const name = el.getName();
|
|
247
|
+
const initializer = el.getInitializer()?.getText();
|
|
248
|
+
if (initializer) {
|
|
249
|
+
defaults.set(name, initializer);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
funcParam
|
|
254
|
+
.getType()
|
|
255
|
+
.getProperties()
|
|
256
|
+
.forEach((prop) => {
|
|
257
|
+
const name = prop.getName();
|
|
258
|
+
const inputTypeDef = typeDefs["input"] ?? {};
|
|
259
|
+
const variable = {
|
|
260
|
+
description: inputTypeDef[name]?.description ?? "",
|
|
261
|
+
type: this.getType(inputTypeDef[name]?.type ?? ""),
|
|
262
|
+
optional: prop.isOptional() ? true : undefined,
|
|
263
|
+
defaultValue: defaults.get(name),
|
|
264
|
+
};
|
|
265
|
+
params[name] = variable;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/* Extract return type from JSDoc */
|
|
269
|
+
// Check if the return type is an object
|
|
270
|
+
if (!funcDecl.getReturnType().isObject()) {
|
|
271
|
+
console.warn(`[Action Registration] Function ${funcName}'s return type should be an object. Skipping...`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const returns = {};
|
|
275
|
+
funcDecl
|
|
276
|
+
.getReturnType()
|
|
277
|
+
.getProperties()
|
|
278
|
+
.forEach((prop) => {
|
|
279
|
+
const name = prop.getName();
|
|
280
|
+
const outputTypeDef = typeDefs["output"] ?? {};
|
|
281
|
+
const variable = {
|
|
282
|
+
description: outputTypeDef[name]?.description ?? "",
|
|
283
|
+
type: this.getType(outputTypeDef[name]?.type ?? ""),
|
|
284
|
+
optional: prop.isOptional() ? true : undefined,
|
|
285
|
+
defaultValue: undefined,
|
|
286
|
+
};
|
|
287
|
+
returns[name] = variable;
|
|
288
|
+
});
|
|
289
|
+
actions.push({
|
|
290
|
+
name: funcName,
|
|
291
|
+
description,
|
|
292
|
+
parameters: params,
|
|
293
|
+
returns,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
// You can now register `actions` in Module Federation or expose them as needed
|
|
298
|
+
console.log("Discovered skill actions:\n", actions.map((a) => "- " + a.name).join("\n"));
|
|
299
|
+
// Register actions in pulse config for runtime access
|
|
300
|
+
this.pulseConfig.actions = actions;
|
|
301
|
+
}
|
|
302
|
+
parseTypeDefs(jsDocs) {
|
|
303
|
+
const typeDefs = {};
|
|
304
|
+
// Match @typedef {Type} Name
|
|
305
|
+
const typedefRegex = /@typedef\s+{([^}]+)}\s+([^\s-]+)/g;
|
|
306
|
+
// Match @property {Type} [name] Description text...
|
|
307
|
+
const propertyRegex = /@property\s+{([^}]+)}\s+(\[?[^\]\s]+\]?)\s*-?\s*(.*)/g;
|
|
308
|
+
jsDocs.forEach((doc) => {
|
|
309
|
+
const text = doc.getFullText();
|
|
310
|
+
let typedefMatches;
|
|
311
|
+
while ((typedefMatches = typedefRegex.exec(text)) !== null) {
|
|
312
|
+
const typeName = typedefMatches[2];
|
|
313
|
+
if (!typeName)
|
|
314
|
+
continue;
|
|
315
|
+
const properties = {};
|
|
316
|
+
let propertyMatches;
|
|
317
|
+
while ((propertyMatches = propertyRegex.exec(text)) !== null) {
|
|
318
|
+
const propName = propertyMatches[2];
|
|
319
|
+
const propType = propertyMatches[1];
|
|
320
|
+
const propDescription = propertyMatches[3] || "";
|
|
321
|
+
if (propName && propType) {
|
|
322
|
+
properties[propName] = {
|
|
323
|
+
type: propType,
|
|
324
|
+
description: propDescription.trim(),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
typeDefs[typeName.toLowerCase()] = properties;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
return typeDefs;
|
|
332
|
+
}
|
|
333
|
+
getType(text) {
|
|
334
|
+
if (text === "string")
|
|
335
|
+
return "string";
|
|
336
|
+
if (text === "number")
|
|
337
|
+
return "number";
|
|
338
|
+
if (text === "boolean")
|
|
339
|
+
return "boolean";
|
|
340
|
+
if (text === "any")
|
|
341
|
+
return "object";
|
|
342
|
+
if (text.endsWith("[]"))
|
|
343
|
+
return [this.getType(text.slice(0, -2))];
|
|
344
|
+
if (text.length === 0)
|
|
345
|
+
return "undefined";
|
|
346
|
+
console.warn(`[Type Warning] Unrecognized type "${text}". Consider adding explicit types in your action's JSDoc comments for better type safety and documentation.`);
|
|
347
|
+
return text;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export async function makeMFServerConfig(mode) {
|
|
351
|
+
const projectDirName = process.cwd();
|
|
352
|
+
const pulseConfig = await loadPulseConfig();
|
|
353
|
+
return {
|
|
354
|
+
mode: mode,
|
|
355
|
+
name: "server",
|
|
356
|
+
entry: {},
|
|
357
|
+
target: "async-node",
|
|
358
|
+
output: {
|
|
359
|
+
publicPath: "auto",
|
|
360
|
+
path: path.resolve(projectDirName, "dist/server"),
|
|
361
|
+
},
|
|
362
|
+
resolve: {
|
|
363
|
+
extensions: [".ts", ".js"],
|
|
364
|
+
},
|
|
365
|
+
plugins: [new MFServerPlugin(pulseConfig)],
|
|
366
|
+
module: {
|
|
367
|
+
rules: [
|
|
368
|
+
{
|
|
369
|
+
test: /\.tsx?$/,
|
|
370
|
+
use: {
|
|
371
|
+
loader: "ts-loader",
|
|
372
|
+
options: {
|
|
373
|
+
configFile: "node_modules/.pulse/tsconfig.server.json",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
exclude: [/node_modules/, /dist/],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
stats: {
|
|
381
|
+
all: false,
|
|
382
|
+
errors: true,
|
|
383
|
+
warnings: true,
|
|
384
|
+
logging: "warn",
|
|
385
|
+
colors: true,
|
|
386
|
+
},
|
|
387
|
+
infrastructureLogging: {
|
|
388
|
+
level: "warn",
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import CopyWebpackPlugin from "copy-webpack-plugin";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
5
|
+
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { getLocalNetworkIP, loadPulseConfig } from "./utils.js";
|
|
8
|
+
class PreviewClientPlugin {
|
|
9
|
+
projectDirName;
|
|
10
|
+
pulseConfig;
|
|
11
|
+
origin;
|
|
12
|
+
constructor(pulseConfig) {
|
|
13
|
+
this.projectDirName = process.cwd();
|
|
14
|
+
this.pulseConfig = pulseConfig;
|
|
15
|
+
this.origin = getLocalNetworkIP();
|
|
16
|
+
}
|
|
17
|
+
apply(compiler) {
|
|
18
|
+
let isFirstRun = true;
|
|
19
|
+
// Before build starts
|
|
20
|
+
compiler.hooks.watchRun.tap("ReloadMessagePlugin", () => {
|
|
21
|
+
if (!isFirstRun) {
|
|
22
|
+
console.log("[client-preview] 🔄 Reloading app...");
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log("[client-preview] 🔄 Building app...");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// After build finishes
|
|
29
|
+
compiler.hooks.done.tap("ReloadMessagePlugin", () => {
|
|
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! 🚀
|
|
38
|
+
`;
|
|
39
|
+
console.log("[client-preview] ✅ Successfully built preview.");
|
|
40
|
+
console.log(previewStartupMessage);
|
|
41
|
+
isFirstRun = false;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log("[client-preview] ✅ Reload finished");
|
|
45
|
+
}
|
|
46
|
+
// Write pulse config to dist
|
|
47
|
+
fs.writeFileSync(path.resolve(this.projectDirName, "dist/pulse.config.json"), JSON.stringify(this.pulseConfig, null, 2));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function makePreviewClientConfig(mode) {
|
|
52
|
+
const projectDirName = process.cwd();
|
|
53
|
+
const pulseConfig = await loadPulseConfig();
|
|
54
|
+
return {
|
|
55
|
+
mode: mode,
|
|
56
|
+
entry: {
|
|
57
|
+
main: "./node_modules/.pulse/server/preview/frontend/index.js",
|
|
58
|
+
},
|
|
59
|
+
output: {
|
|
60
|
+
path: path.resolve(projectDirName, "dist/client"),
|
|
61
|
+
},
|
|
62
|
+
resolve: {
|
|
63
|
+
extensions: [".ts", ".tsx", ".js"],
|
|
64
|
+
},
|
|
65
|
+
plugins: [
|
|
66
|
+
new HtmlWebpackPlugin({
|
|
67
|
+
template: "./node_modules/.pulse/server/preview/frontend/index.html",
|
|
68
|
+
}),
|
|
69
|
+
new MiniCssExtractPlugin({
|
|
70
|
+
filename: "globals.css",
|
|
71
|
+
}),
|
|
72
|
+
new CopyWebpackPlugin({
|
|
73
|
+
patterns: [{ from: "src/assets", to: "assets" }],
|
|
74
|
+
}),
|
|
75
|
+
new PreviewClientPlugin(pulseConfig),
|
|
76
|
+
],
|
|
77
|
+
watchOptions: {
|
|
78
|
+
ignored: /src\/server-function/,
|
|
79
|
+
},
|
|
80
|
+
module: {
|
|
81
|
+
rules: [
|
|
82
|
+
{
|
|
83
|
+
test: /\.tsx?$/,
|
|
84
|
+
use: "ts-loader",
|
|
85
|
+
exclude: [/node_modules/, /dist/],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
test: /\.css$/i,
|
|
89
|
+
use: [
|
|
90
|
+
MiniCssExtractPlugin.loader,
|
|
91
|
+
"css-loader",
|
|
92
|
+
{
|
|
93
|
+
loader: "postcss-loader",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
stats: {
|
|
100
|
+
all: false,
|
|
101
|
+
errors: true,
|
|
102
|
+
warnings: true,
|
|
103
|
+
logging: "warn",
|
|
104
|
+
colors: true,
|
|
105
|
+
},
|
|
106
|
+
infrastructureLogging: {
|
|
107
|
+
level: "warn",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { globSync } from "glob";
|
|
5
|
+
import { networkInterfaces } from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
export async function loadPulseConfig() {
|
|
11
|
+
const projectDirName = process.cwd();
|
|
12
|
+
// compile to js file and import
|
|
13
|
+
const program = ts.createProgram({
|
|
14
|
+
rootNames: [path.join(projectDirName, "pulse.config.ts")],
|
|
15
|
+
options: {
|
|
16
|
+
module: ts.ModuleKind.ESNext,
|
|
17
|
+
target: ts.ScriptTarget.ES2020,
|
|
18
|
+
outDir: path.join(projectDirName, "node_modules/.pulse/config"),
|
|
19
|
+
esModuleInterop: true,
|
|
20
|
+
skipLibCheck: true,
|
|
21
|
+
forceConsistentCasingInFileNames: true,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
program.emit();
|
|
25
|
+
// Fix imports in the generated js file for all files in node_modules/.pulse/config
|
|
26
|
+
globSync("node_modules/.pulse/config/**/*.js", {
|
|
27
|
+
cwd: projectDirName,
|
|
28
|
+
absolute: true,
|
|
29
|
+
}).forEach(async (jsFile) => {
|
|
30
|
+
let content = await fs.readFile(jsFile, "utf-8");
|
|
31
|
+
content = content.replace(/(from\s+["']\.\/[^\s"']+)(["'])/g, (match, p1, p2) => {
|
|
32
|
+
// No change if the import already has any extension
|
|
33
|
+
if (p1.match(/\.(js|cjs|mjs|ts|tsx|json)$/)) {
|
|
34
|
+
return match; // No change needed
|
|
35
|
+
}
|
|
36
|
+
return `${p1}.js${p2}`;
|
|
37
|
+
});
|
|
38
|
+
await fs.writeFile(jsFile, content);
|
|
39
|
+
});
|
|
40
|
+
// Copy package.json if exists
|
|
41
|
+
const pkgPath = path.join(projectDirName, "package.json");
|
|
42
|
+
if (existsSync(pkgPath)) {
|
|
43
|
+
const destPath = path.join(projectDirName, "node_modules/.pulse/config/package.json");
|
|
44
|
+
await fs.copyFile(pkgPath, destPath);
|
|
45
|
+
}
|
|
46
|
+
const compiledConfig = path.join(projectDirName, "node_modules/.pulse/config/pulse.config.js");
|
|
47
|
+
const mod = await import(pathToFileURL(compiledConfig).href);
|
|
48
|
+
// delete the compiled config after importing
|
|
49
|
+
await fs.rm(path.join(projectDirName, "node_modules/.pulse/config"), {
|
|
50
|
+
recursive: true,
|
|
51
|
+
force: true,
|
|
52
|
+
});
|
|
53
|
+
return mod.default;
|
|
54
|
+
}
|
|
55
|
+
export function getLocalNetworkIP() {
|
|
56
|
+
const interfaces = networkInterfaces();
|
|
57
|
+
for (const iface of Object.values(interfaces)) {
|
|
58
|
+
if (!iface)
|
|
59
|
+
continue;
|
|
60
|
+
for (const config of iface) {
|
|
61
|
+
if (config.family === "IPv4" && !config.internal) {
|
|
62
|
+
return config.address; // Returns the first non-internal IPv4 address
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return "localhost"; // Fallback
|
|
67
|
+
}
|
|
68
|
+
export async function readConfigFile() {
|
|
69
|
+
// Read pulse.config.json from dist/client
|
|
70
|
+
// Wait until dist/pulse.config.json exists
|
|
71
|
+
while (true) {
|
|
72
|
+
try {
|
|
73
|
+
await fs.access("dist/pulse.config.json");
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// Wait for 100ms before trying again
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const data = await fs.readFile("dist/pulse.config.json", "utf-8");
|
|
82
|
+
return JSON.parse(data);
|
|
83
|
+
}
|
|
84
|
+
export function discoverAppSkillActions() {
|
|
85
|
+
// Get all .ts files under src/skill and read use default exports as entry points
|
|
86
|
+
const files = globSync("./src/skill/*/action.ts");
|
|
87
|
+
const entryPoints = files
|
|
88
|
+
.map((file) => file.replaceAll("\\", "/"))
|
|
89
|
+
.map((file) => {
|
|
90
|
+
// Read default export info in the file using ts-morph to get the function name, and use the function name as the entry point key instead of the file name
|
|
91
|
+
const project = new Project({
|
|
92
|
+
tsConfigFilePath: path.join(process.cwd(), "node_modules/.pulse/tsconfig.server.json"),
|
|
93
|
+
});
|
|
94
|
+
const sourceFile = project.addSourceFileAtPath(file);
|
|
95
|
+
const defaultExportSymbol = sourceFile.getDefaultExportSymbol();
|
|
96
|
+
if (!defaultExportSymbol) {
|
|
97
|
+
console.warn(`No default export found in action file ${file}. Skipping...`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const defaultExportDeclarations = defaultExportSymbol.getDeclarations();
|
|
101
|
+
if (defaultExportDeclarations.length === 0) {
|
|
102
|
+
console.warn(`No declarations found for default export in action file ${file}. Skipping...`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const funcDecl = defaultExportDeclarations[0]?.asKind(SyntaxKind.FunctionDeclaration);
|
|
106
|
+
if (!funcDecl) {
|
|
107
|
+
console.warn(`Default export in action file ${file} is not a function declaration. Skipping...`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
["./skill/" + funcDecl.getName()]: "./" + file,
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
.reduce((acc, curr) => {
|
|
115
|
+
return { ...acc, ...curr };
|
|
116
|
+
}, {});
|
|
117
|
+
return entryPoints;
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createWebpackConfig(isPreview: boolean, buildTarget: "client" | "server" | "both", mode: "development" | "production"): Promise<import("webpack").Configuration[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { makeMFClientConfig } from "./configs/mf-client.js";
|
|
3
|
+
import { makeMFServerConfig } from "./configs/mf-server.js";
|
|
4
|
+
import { makePreviewClientConfig } from "./configs/preview.js";
|
|
5
|
+
export async function createWebpackConfig(isPreview, buildTarget, mode) {
|
|
6
|
+
if (isPreview) {
|
|
7
|
+
const previewClientConfig = await makePreviewClientConfig("development");
|
|
8
|
+
const mfServerConfig = await makeMFServerConfig("development");
|
|
9
|
+
return [previewClientConfig, mfServerConfig];
|
|
10
|
+
}
|
|
11
|
+
else if (buildTarget === "server") {
|
|
12
|
+
const mfServerConfig = await makeMFServerConfig(mode);
|
|
13
|
+
return [mfServerConfig];
|
|
14
|
+
}
|
|
15
|
+
else if (buildTarget === "client") {
|
|
16
|
+
const mfClientConfig = await makeMFClientConfig(mode);
|
|
17
|
+
return [mfClientConfig];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const mfClientConfig = await makeMFClientConfig(mode);
|
|
21
|
+
const mfServerConfig = await makeMFServerConfig(mode);
|
|
22
|
+
return [mfClientConfig, mfServerConfig];
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-editor/cli",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.36",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pulse": "dist/cli.js"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@module-federation/enhanced": "2.0.1",
|
|
23
23
|
"@module-federation/node": "2.7.32",
|
|
24
24
|
"@module-federation/runtime": "2.0.1",
|
|
25
|
-
"@pulse-editor/shared-utils": "^0.1.1-beta.
|
|
25
|
+
"@pulse-editor/shared-utils": "^0.1.1-beta.76",
|
|
26
26
|
"concurrently": "^9.2.1",
|
|
27
27
|
"connect-livereload": "^0.6.1",
|
|
28
28
|
"copy-webpack-plugin": "^13.0.1",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"mini-css-extract-plugin": "^2.10.0",
|
|
43
43
|
"openid-client": "^6.8.2",
|
|
44
44
|
"rimraf": "^6.1.3",
|
|
45
|
+
"ts-morph": "^27.0.2",
|
|
45
46
|
"tsx": "^4.21.0",
|
|
46
47
|
"webpack": "^5.105.2",
|
|
47
48
|
"webpack-dev-server": "^5.2.3"
|
|
@@ -54,14 +55,12 @@
|
|
|
54
55
|
"@types/livereload": "^0.9.5",
|
|
55
56
|
"@types/react": "^19.2.14",
|
|
56
57
|
"@types/react-dom": "^19.2.3",
|
|
57
|
-
"@vdemedes/prettier-config": "^2.0.1",
|
|
58
58
|
"ava": "^6.4.1",
|
|
59
59
|
"chalk": "^5.6.2",
|
|
60
60
|
"eslint-config-xo-react": "^0.29.0",
|
|
61
61
|
"eslint-plugin-react": "^7.37.5",
|
|
62
62
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
63
63
|
"ink-testing-library": "^4.0.0",
|
|
64
|
-
"prettier": "^3.8.1",
|
|
65
64
|
"react": "19.2.4",
|
|
66
65
|
"react-dom": "19.2.4",
|
|
67
66
|
"ts-node": "^10.9.2",
|
|
@@ -87,6 +86,5 @@
|
|
|
87
86
|
"rules": {
|
|
88
87
|
"react/prop-types": "off"
|
|
89
88
|
}
|
|
90
|
-
}
|
|
91
|
-
"prettier": "@vdemedes/prettier-config"
|
|
89
|
+
}
|
|
92
90
|
}
|