mn-rails-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +1760 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1737 -0
- package/package.json +55 -0
- package/src/templates/basic/README.md +29 -0
- package/src/templates/basic/mn-rails.config.js +6 -0
- package/src/templates/basic/package.json.template +17 -0
- package/src/templates/basic/src/controllers/MainController.ts +16 -0
- package/src/templates/basic/src/index.ts +18 -0
- package/src/templates/basic/tsconfig.json +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1737 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/new.ts
|
|
7
|
+
import * as fs from "fs-extra";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import inquirer from "inquirer";
|
|
11
|
+
async function newCommand(name, options = {}) {
|
|
12
|
+
try {
|
|
13
|
+
let projectName = name;
|
|
14
|
+
if (!projectName) {
|
|
15
|
+
const answer = await inquirer.prompt([
|
|
16
|
+
{
|
|
17
|
+
type: "input",
|
|
18
|
+
name: "name",
|
|
19
|
+
message: "Project name:",
|
|
20
|
+
validate: (input) => {
|
|
21
|
+
if (!input.trim()) {
|
|
22
|
+
return "Project name cannot be empty";
|
|
23
|
+
}
|
|
24
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
25
|
+
return "Project name can only contain lowercase letters, numbers, and hyphens";
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
projectName = answer.name;
|
|
32
|
+
}
|
|
33
|
+
if (!projectName || typeof projectName !== "string") {
|
|
34
|
+
console.error(chalk.red("Error: Project name is required"));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
38
|
+
if (fs.existsSync(projectPath)) {
|
|
39
|
+
console.error(chalk.red(`Error: Directory "${projectName}" already exists`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.blue(`Creating new plugin project: ${projectName}...`));
|
|
43
|
+
fs.ensureDirSync(projectPath);
|
|
44
|
+
const templateName = options.template || "basic";
|
|
45
|
+
const templatePath = path.resolve(__dirname, "../templates", templateName);
|
|
46
|
+
if (!fs.existsSync(templatePath)) {
|
|
47
|
+
console.warn(chalk.yellow(`Template "${templateName}" not found, using basic template`));
|
|
48
|
+
await createBasicTemplate(projectPath, projectName);
|
|
49
|
+
} else {
|
|
50
|
+
await copyTemplate(templatePath, projectPath, projectName);
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.green(`\u2713 Project created successfully!`));
|
|
53
|
+
console.log(chalk.cyan(`
|
|
54
|
+
Next steps:`));
|
|
55
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
56
|
+
console.log(chalk.cyan(` npm install`));
|
|
57
|
+
console.log(chalk.cyan(` mn-rails build`));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function createBasicTemplate(projectPath, projectName) {
|
|
64
|
+
const dirs = ["src", "src/controllers", "src/views"];
|
|
65
|
+
dirs.forEach((dir) => fs.ensureDirSync(path.join(projectPath, dir)));
|
|
66
|
+
const packageJson = {
|
|
67
|
+
name: projectName,
|
|
68
|
+
version: "0.1.0",
|
|
69
|
+
description: "A MarginNote plugin",
|
|
70
|
+
type: "module",
|
|
71
|
+
main: "dist/index.js",
|
|
72
|
+
scripts: {
|
|
73
|
+
build: "mn-rails build",
|
|
74
|
+
dev: "mn-rails dev"
|
|
75
|
+
},
|
|
76
|
+
dependencies: {
|
|
77
|
+
"mn-rails-core": "workspace:*"
|
|
78
|
+
},
|
|
79
|
+
devDependencies: {
|
|
80
|
+
typescript: "^5.3.3"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
fs.writeJSONSync(path.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
84
|
+
const tsconfig = {
|
|
85
|
+
extends: "mn-rails-core/tsconfig.json",
|
|
86
|
+
compilerOptions: {
|
|
87
|
+
outDir: "./dist",
|
|
88
|
+
rootDir: "./src"
|
|
89
|
+
},
|
|
90
|
+
include: ["src/**/*"]
|
|
91
|
+
};
|
|
92
|
+
fs.writeJSONSync(path.join(projectPath, "tsconfig.json"), tsconfig, { spaces: 2 });
|
|
93
|
+
const config = `export default {
|
|
94
|
+
// Plugin configuration
|
|
95
|
+
name: '${projectName}',
|
|
96
|
+
version: '0.1.0',
|
|
97
|
+
};
|
|
98
|
+
`;
|
|
99
|
+
fs.writeFileSync(path.join(projectPath, "mn-rails.config.js"), config);
|
|
100
|
+
const indexContent = `import { App, MN } from 'mn-rails-core';
|
|
101
|
+
import MainController from './controllers/MainController.js';
|
|
102
|
+
|
|
103
|
+
class Plugin extends App {
|
|
104
|
+
private controller?: MainController;
|
|
105
|
+
|
|
106
|
+
onLaunch() {
|
|
107
|
+
MN.log('Plugin launched!');
|
|
108
|
+
this.controller = new MainController();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onExit() {
|
|
112
|
+
MN.log('Plugin exiting...');
|
|
113
|
+
this.controller?.onDestroy?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default new Plugin();
|
|
118
|
+
`;
|
|
119
|
+
fs.writeFileSync(path.join(projectPath, "src/index.ts"), indexContent);
|
|
120
|
+
const controllerContent = `import { Controller, MN } from 'mn-rails-core';
|
|
121
|
+
|
|
122
|
+
export default class MainController extends Controller {
|
|
123
|
+
onInit() {
|
|
124
|
+
MN.log('MainController initialized - Hello World!');
|
|
125
|
+
|
|
126
|
+
// \u793A\u4F8B\uFF1A\u521B\u5EFA\u539F\u751F\u6309\u94AE
|
|
127
|
+
const button = MN.nativeUI.createButton({
|
|
128
|
+
title: 'Hello World',
|
|
129
|
+
style: 'primary',
|
|
130
|
+
frame: { x: 10, y: 10, width: 150, height: 44 },
|
|
131
|
+
action: async () => {
|
|
132
|
+
// \u663E\u793A\u63D0\u793A\u6846
|
|
133
|
+
MN.alert('Hello World!\\n\u6B22\u8FCE\u4F7F\u7528 MN Rails');
|
|
134
|
+
|
|
135
|
+
// \u6253\u5F00 WebView \u9762\u677F
|
|
136
|
+
const webView = await MN.webUI.render('HelloWorld', {
|
|
137
|
+
message: 'Hello from Controller!'
|
|
138
|
+
}, {
|
|
139
|
+
x: 100,
|
|
140
|
+
y: 100,
|
|
141
|
+
width: 400,
|
|
142
|
+
height: 300
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (webView) {
|
|
146
|
+
MN.log('WebView opened:', webView.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (button) {
|
|
152
|
+
MN.log('Native button created');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// \u5B9A\u4E49\u4F9B WebView \u8C03\u7528\u7684 actions\uFF08\u7C7B\u578B\u5B89\u5168\uFF09
|
|
157
|
+
actions = {
|
|
158
|
+
/**
|
|
159
|
+
* Hello World \u793A\u4F8B\uFF1A\u5411\u67D0\u4EBA\u95EE\u597D
|
|
160
|
+
*/
|
|
161
|
+
async hello(name: string): Promise<string> {
|
|
162
|
+
const message = \`Hello, \${name}! Welcome to MN Rails!\`;
|
|
163
|
+
MN.log(message);
|
|
164
|
+
return message;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* \u83B7\u53D6\u5F53\u524D\u65F6\u95F4
|
|
169
|
+
*/
|
|
170
|
+
async getCurrentTime(): Promise<string> {
|
|
171
|
+
return new Date().toLocaleString();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
fs.writeFileSync(path.join(projectPath, "src/controllers/MainController.ts"), controllerContent);
|
|
177
|
+
const helloWorldView = `<!DOCTYPE html>
|
|
178
|
+
<html>
|
|
179
|
+
<head>
|
|
180
|
+
<meta charset="UTF-8">
|
|
181
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
182
|
+
<title>Hello World - MN Rails</title>
|
|
183
|
+
<style>
|
|
184
|
+
body {
|
|
185
|
+
margin: 0;
|
|
186
|
+
padding: 20px;
|
|
187
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
188
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
189
|
+
color: white;
|
|
190
|
+
min-height: 100vh;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
}
|
|
196
|
+
.container {
|
|
197
|
+
text-align: center;
|
|
198
|
+
max-width: 400px;
|
|
199
|
+
}
|
|
200
|
+
h1 {
|
|
201
|
+
font-size: 2.5em;
|
|
202
|
+
margin: 0 0 20px 0;
|
|
203
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
204
|
+
}
|
|
205
|
+
.message {
|
|
206
|
+
font-size: 1.2em;
|
|
207
|
+
margin: 20px 0;
|
|
208
|
+
padding: 15px;
|
|
209
|
+
background: rgba(255,255,255,0.2);
|
|
210
|
+
border-radius: 10px;
|
|
211
|
+
backdrop-filter: blur(10px);
|
|
212
|
+
}
|
|
213
|
+
button {
|
|
214
|
+
background: white;
|
|
215
|
+
color: #667eea;
|
|
216
|
+
border: none;
|
|
217
|
+
padding: 12px 24px;
|
|
218
|
+
font-size: 1em;
|
|
219
|
+
border-radius: 25px;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
margin: 10px;
|
|
222
|
+
font-weight: bold;
|
|
223
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
|
|
224
|
+
transition: transform 0.2s;
|
|
225
|
+
}
|
|
226
|
+
button:hover {
|
|
227
|
+
transform: translateY(-2px);
|
|
228
|
+
box-shadow: 0 6px 8px rgba(0,0,0,0.3);
|
|
229
|
+
}
|
|
230
|
+
button:active {
|
|
231
|
+
transform: translateY(0);
|
|
232
|
+
}
|
|
233
|
+
.result {
|
|
234
|
+
margin-top: 20px;
|
|
235
|
+
padding: 15px;
|
|
236
|
+
background: rgba(255,255,255,0.1);
|
|
237
|
+
border-radius: 10px;
|
|
238
|
+
min-height: 20px;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
241
|
+
</head>
|
|
242
|
+
<body>
|
|
243
|
+
<div class="container">
|
|
244
|
+
<h1>\u{1F44B} Hello World!</h1>
|
|
245
|
+
<div class="message" id="message">
|
|
246
|
+
<p>\u6B22\u8FCE\u4F7F\u7528 MN Rails \u6846\u67B6\uFF01</p>
|
|
247
|
+
<p id="props-message"></p>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<button onclick="handleHello()">\u8C03\u7528 Controller</button>
|
|
251
|
+
<button onclick="handleGetTime()">\u83B7\u53D6\u65F6\u95F4</button>
|
|
252
|
+
|
|
253
|
+
<div class="result" id="result"></div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
// \u663E\u793A\u4ECE Controller \u4F20\u9012\u7684 props
|
|
258
|
+
if (window.__MN_RAILS_PROPS__) {
|
|
259
|
+
const propsMsg = document.getElementById('props-message');
|
|
260
|
+
if (propsMsg && window.__MN_RAILS_PROPS__.message) {
|
|
261
|
+
propsMsg.textContent = window.__MN_RAILS_PROPS__.message;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// \u8C03\u7528 Controller \u7684 hello action
|
|
266
|
+
async function handleHello() {
|
|
267
|
+
const resultEl = document.getElementById('result');
|
|
268
|
+
try {
|
|
269
|
+
if (window.bridge) {
|
|
270
|
+
resultEl.textContent = '\u8C03\u7528\u4E2D...';
|
|
271
|
+
const result = await window.bridge.main.hello('MN Rails \u5F00\u53D1\u8005');
|
|
272
|
+
resultEl.textContent = '\u7ED3\u679C: ' + result;
|
|
273
|
+
resultEl.style.color = '#4ade80';
|
|
274
|
+
} else {
|
|
275
|
+
resultEl.textContent = '\u9519\u8BEF: bridge \u5BF9\u8C61\u4E0D\u53EF\u7528';
|
|
276
|
+
resultEl.style.color = '#f87171';
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
resultEl.textContent = '\u9519\u8BEF: ' + error.message;
|
|
280
|
+
resultEl.style.color = '#f87171';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// \u8C03\u7528 Controller \u7684 getCurrentTime action
|
|
285
|
+
async function handleGetTime() {
|
|
286
|
+
const resultEl = document.getElementById('result');
|
|
287
|
+
try {
|
|
288
|
+
if (window.bridge) {
|
|
289
|
+
resultEl.textContent = '\u83B7\u53D6\u4E2D...';
|
|
290
|
+
const time = await window.bridge.main.getCurrentTime();
|
|
291
|
+
resultEl.textContent = '\u5F53\u524D\u65F6\u95F4: ' + time;
|
|
292
|
+
resultEl.style.color = '#4ade80';
|
|
293
|
+
} else {
|
|
294
|
+
resultEl.textContent = '\u9519\u8BEF: bridge \u5BF9\u8C61\u4E0D\u53EF\u7528';
|
|
295
|
+
resultEl.style.color = '#f87171';
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
resultEl.textContent = '\u9519\u8BEF: ' + error.message;
|
|
299
|
+
resultEl.style.color = '#f87171';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// \u9875\u9762\u52A0\u8F7D\u5B8C\u6210\u540E\u7684\u63D0\u793A
|
|
304
|
+
window.addEventListener('load', () => {
|
|
305
|
+
console.log('Hello World WebView loaded!');
|
|
306
|
+
if (window.bridge) {
|
|
307
|
+
console.log('\u2713 Bridge is available');
|
|
308
|
+
} else {
|
|
309
|
+
console.warn('\u26A0 Bridge is not available');
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
</script>
|
|
313
|
+
</body>
|
|
314
|
+
</html>
|
|
315
|
+
`;
|
|
316
|
+
fs.writeFileSync(path.join(projectPath, "src/views/HelloWorld.html"), helloWorldView);
|
|
317
|
+
const readme = `# ${projectName}
|
|
318
|
+
|
|
319
|
+
A MarginNote plugin built with MN Rails.
|
|
320
|
+
|
|
321
|
+
## Development
|
|
322
|
+
|
|
323
|
+
\`\`\`bash
|
|
324
|
+
npm install
|
|
325
|
+
mn-rails build
|
|
326
|
+
\`\`\`
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT
|
|
331
|
+
`;
|
|
332
|
+
fs.writeFileSync(path.join(projectPath, "README.md"), readme);
|
|
333
|
+
}
|
|
334
|
+
async function copyTemplate(templatePath, projectPath, projectName) {
|
|
335
|
+
const files = fs.readdirSync(templatePath, { withFileTypes: true });
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
const srcPath = path.join(templatePath, file.name);
|
|
338
|
+
let destPath = path.join(projectPath, file.name);
|
|
339
|
+
if (file.name.endsWith(".template")) {
|
|
340
|
+
destPath = destPath.replace(".template", "");
|
|
341
|
+
}
|
|
342
|
+
if (file.isDirectory()) {
|
|
343
|
+
fs.copySync(srcPath, destPath);
|
|
344
|
+
} else {
|
|
345
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
346
|
+
const projectTitle = projectName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
347
|
+
content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName).replace(/\{\{PROJECT_TITLE\}\}/g, projectTitle);
|
|
348
|
+
fs.writeFileSync(destPath, content);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const dirs = ["src", "src/controllers", "src/views"];
|
|
352
|
+
dirs.forEach((dir) => {
|
|
353
|
+
const dirPath = path.join(projectPath, dir);
|
|
354
|
+
if (!fs.existsSync(dirPath)) {
|
|
355
|
+
fs.ensureDirSync(dirPath);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/commands/build.ts
|
|
361
|
+
import * as path5 from "path";
|
|
362
|
+
import * as fs4 from "fs-extra";
|
|
363
|
+
import chalk3 from "chalk";
|
|
364
|
+
|
|
365
|
+
// src/build/index.ts
|
|
366
|
+
import * as path4 from "path";
|
|
367
|
+
import * as fs3 from "fs-extra";
|
|
368
|
+
import { execSync } from "child_process";
|
|
369
|
+
import chalk2 from "chalk";
|
|
370
|
+
|
|
371
|
+
// src/utils/config.ts
|
|
372
|
+
import * as path2 from "path";
|
|
373
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
374
|
+
async function loadConfig(projectRoot) {
|
|
375
|
+
const defaultConfig = {
|
|
376
|
+
name: path2.basename(projectRoot),
|
|
377
|
+
version: "0.1.0",
|
|
378
|
+
title: path2.basename(projectRoot),
|
|
379
|
+
author: ""
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
const explorer = cosmiconfig("mn-rails", {
|
|
383
|
+
searchPlaces: ["mn-rails.config.js", "mn-rails.config.ts", "mn-rails.config.mjs", "mn-rails.config.cjs"]
|
|
384
|
+
});
|
|
385
|
+
const result = await explorer.search(projectRoot);
|
|
386
|
+
if (result && result.config) {
|
|
387
|
+
return { ...defaultConfig, ...result.config };
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.warn(`Warning: Could not load config file: ${error}`);
|
|
391
|
+
}
|
|
392
|
+
return defaultConfig;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/dev/companion.ts
|
|
396
|
+
function generateCompanionCode(options) {
|
|
397
|
+
const {
|
|
398
|
+
serverUrl,
|
|
399
|
+
reconnectInterval = 3e3,
|
|
400
|
+
maxReconnectAttempts = 10
|
|
401
|
+
} = options;
|
|
402
|
+
return `
|
|
403
|
+
// MN Rails Debug Companion - Auto-generated, do not edit
|
|
404
|
+
(function() {
|
|
405
|
+
'use strict';
|
|
406
|
+
|
|
407
|
+
const SERVER_URL = '${serverUrl}';
|
|
408
|
+
const RECONNECT_INTERVAL = ${reconnectInterval};
|
|
409
|
+
const MAX_RECONNECT_ATTEMPTS = ${maxReconnectAttempts};
|
|
410
|
+
|
|
411
|
+
let ws = null;
|
|
412
|
+
let reconnectAttempts = 0;
|
|
413
|
+
let reconnectTimer = null;
|
|
414
|
+
let isConnected = false;
|
|
415
|
+
let messageQueue = [];
|
|
416
|
+
|
|
417
|
+
// \u539F\u59CB console \u65B9\u6CD5
|
|
418
|
+
const originalConsole = {
|
|
419
|
+
log: console.log,
|
|
420
|
+
error: console.error,
|
|
421
|
+
warn: console.warn,
|
|
422
|
+
info: console.info
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// \u53D1\u9001\u6D88\u606F\u5230\u670D\u52A1\u5668
|
|
426
|
+
function sendMessage(type, payload) {
|
|
427
|
+
const message = {
|
|
428
|
+
type: type,
|
|
429
|
+
payload: payload,
|
|
430
|
+
timestamp: Date.now()
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (isConnected && ws && ws.readyState === 1) {
|
|
434
|
+
try {
|
|
435
|
+
ws.send(JSON.stringify(message));
|
|
436
|
+
return true;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error('Failed to send message:', error);
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
// \u5982\u679C\u672A\u8FDE\u63A5\uFF0C\u5C06\u6D88\u606F\u52A0\u5165\u961F\u5217
|
|
443
|
+
messageQueue.push(message);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// \u5904\u7406\u6D88\u606F\u961F\u5217
|
|
449
|
+
function processMessageQueue() {
|
|
450
|
+
while (messageQueue.length > 0 && isConnected) {
|
|
451
|
+
const message = messageQueue.shift();
|
|
452
|
+
if (message && ws && ws.readyState === 1) {
|
|
453
|
+
try {
|
|
454
|
+
ws.send(JSON.stringify(message));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// \u53D1\u9001\u5931\u8D25\uFF0C\u91CD\u65B0\u52A0\u5165\u961F\u5217
|
|
457
|
+
messageQueue.unshift(message);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// \u62E6\u622A console.log
|
|
465
|
+
console.log = function(...args) {
|
|
466
|
+
originalConsole.log.apply(console, arguments);
|
|
467
|
+
sendMessage('log', {
|
|
468
|
+
level: 'log',
|
|
469
|
+
args: args.map(arg => {
|
|
470
|
+
if (typeof arg === 'object') {
|
|
471
|
+
try {
|
|
472
|
+
return JSON.stringify(arg);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
return String(arg);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return String(arg);
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// \u62E6\u622A console.error
|
|
483
|
+
console.error = function(...args) {
|
|
484
|
+
originalConsole.error.apply(console, arguments);
|
|
485
|
+
sendMessage('error', {
|
|
486
|
+
level: 'error',
|
|
487
|
+
args: args.map(arg => {
|
|
488
|
+
if (typeof arg === 'object') {
|
|
489
|
+
try {
|
|
490
|
+
return JSON.stringify(arg);
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return String(arg);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return String(arg);
|
|
496
|
+
})
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// \u62E6\u622A console.warn
|
|
501
|
+
console.warn = function(...args) {
|
|
502
|
+
originalConsole.warn.apply(console, arguments);
|
|
503
|
+
sendMessage('warn', {
|
|
504
|
+
level: 'warn',
|
|
505
|
+
args: args.map(arg => {
|
|
506
|
+
if (typeof arg === 'object') {
|
|
507
|
+
try {
|
|
508
|
+
return JSON.stringify(arg);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
return String(arg);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return String(arg);
|
|
514
|
+
})
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// \u8FDE\u63A5 WebSocket \u670D\u52A1\u5668
|
|
519
|
+
function connect() {
|
|
520
|
+
try {
|
|
521
|
+
if (ws) {
|
|
522
|
+
ws.close();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
ws = new WebSocket(SERVER_URL);
|
|
526
|
+
|
|
527
|
+
ws.onopen = function() {
|
|
528
|
+
isConnected = true;
|
|
529
|
+
reconnectAttempts = 0;
|
|
530
|
+
sendMessage('ready', { message: 'Debug companion connected' });
|
|
531
|
+
processMessageQueue();
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
ws.onmessage = function(event) {
|
|
535
|
+
try {
|
|
536
|
+
const message = JSON.parse(event.data);
|
|
537
|
+
handleServerMessage(message);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error('Failed to parse server message:', error);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
ws.onerror = function(error) {
|
|
544
|
+
console.error('WebSocket error:', error);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
ws.onclose = function() {
|
|
548
|
+
isConnected = false;
|
|
549
|
+
ws = null;
|
|
550
|
+
|
|
551
|
+
// \u5C1D\u8BD5\u91CD\u8FDE
|
|
552
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
553
|
+
reconnectAttempts++;
|
|
554
|
+
reconnectTimer = setTimeout(function() {
|
|
555
|
+
connect();
|
|
556
|
+
}, RECONNECT_INTERVAL);
|
|
557
|
+
} else {
|
|
558
|
+
console.error('Max reconnect attempts reached. Debug companion disconnected.');
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error('Failed to create WebSocket connection:', error);
|
|
563
|
+
// \u5C1D\u8BD5\u91CD\u8FDE
|
|
564
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
565
|
+
reconnectAttempts++;
|
|
566
|
+
reconnectTimer = setTimeout(function() {
|
|
567
|
+
connect();
|
|
568
|
+
}, RECONNECT_INTERVAL);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// \u5B58\u50A8\u63D2\u4EF6\u5B9E\u4F8B\u548C\u6E05\u7406\u51FD\u6570
|
|
574
|
+
let pluginInstance = null;
|
|
575
|
+
let cleanupFunctions = [];
|
|
576
|
+
|
|
577
|
+
// \u6CE8\u518C\u6E05\u7406\u51FD\u6570
|
|
578
|
+
function registerCleanup(fn) {
|
|
579
|
+
cleanupFunctions.push(fn);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// \u6267\u884C\u6E05\u7406
|
|
583
|
+
function cleanup() {
|
|
584
|
+
cleanupFunctions.forEach(fn => {
|
|
585
|
+
try {
|
|
586
|
+
fn();
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error('Cleanup error:', error);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
cleanupFunctions = [];
|
|
592
|
+
pluginInstance = null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// \u5904\u7406\u670D\u52A1\u5668\u6D88\u606F
|
|
596
|
+
function handleServerMessage(message) {
|
|
597
|
+
switch (message.type) {
|
|
598
|
+
case 'heartbeat':
|
|
599
|
+
// \u54CD\u5E94\u5FC3\u8DF3
|
|
600
|
+
sendMessage('heartbeat', { timestamp: Date.now() });
|
|
601
|
+
break;
|
|
602
|
+
|
|
603
|
+
case 'code-update':
|
|
604
|
+
// \u4EE3\u7801\u66F4\u65B0\u901A\u77E5
|
|
605
|
+
if (message.payload && message.payload.code) {
|
|
606
|
+
try {
|
|
607
|
+
// \u6267\u884C\u6E05\u7406
|
|
608
|
+
cleanup();
|
|
609
|
+
|
|
610
|
+
// \u6267\u884C\u65B0\u4EE3\u7801
|
|
611
|
+
const newCode = message.payload.code;
|
|
612
|
+
const executeCode = new Function(newCode);
|
|
613
|
+
executeCode();
|
|
614
|
+
|
|
615
|
+
console.log('Plugin reloaded successfully');
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error('Failed to reload plugin:', error);
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
// \u5982\u679C\u6CA1\u6709\u4EE3\u7801\uFF0C\u53EA\u662F\u901A\u77E5\u66F4\u65B0
|
|
621
|
+
console.log('Code update detected, please restart the plugin');
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
624
|
+
|
|
625
|
+
default:
|
|
626
|
+
// \u5FFD\u7565\u672A\u77E5\u6D88\u606F\u7C7B\u578B
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// \u521D\u59CB\u5316\u8FDE\u63A5
|
|
632
|
+
connect();
|
|
633
|
+
|
|
634
|
+
// \u5BFC\u51FA\u8FDE\u63A5\u72B6\u6001\u548C\u6E05\u7406\u51FD\u6570\uFF08\u7528\u4E8E\u70ED\u91CD\u8F7D\uFF09
|
|
635
|
+
if (typeof window !== 'undefined') {
|
|
636
|
+
window.__MN_RAILS_DEBUG__ = {
|
|
637
|
+
isConnected: function() { return isConnected; },
|
|
638
|
+
reconnect: connect,
|
|
639
|
+
sendMessage: sendMessage,
|
|
640
|
+
registerCleanup: registerCleanup,
|
|
641
|
+
cleanup: cleanup
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// \u5168\u5C40\u5BFC\u51FA\uFF08\u7528\u4E8E MarginNote \u73AF\u5883\uFF09
|
|
646
|
+
if (typeof global !== 'undefined') {
|
|
647
|
+
global.__MN_RAILS_DEBUG__ = {
|
|
648
|
+
isConnected: function() { return isConnected; },
|
|
649
|
+
reconnect: connect,
|
|
650
|
+
sendMessage: sendMessage,
|
|
651
|
+
registerCleanup: registerCleanup,
|
|
652
|
+
cleanup: cleanup
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
})();
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/build/type-generator.ts
|
|
660
|
+
import * as fs2 from "fs-extra";
|
|
661
|
+
import * as path3 from "path";
|
|
662
|
+
import { Project } from "ts-morph";
|
|
663
|
+
function extractActionsType(objectLiteral, sourceFile) {
|
|
664
|
+
const signatures = [];
|
|
665
|
+
const properties = objectLiteral.getProperties();
|
|
666
|
+
for (const prop of properties) {
|
|
667
|
+
if (prop.getKindName() === "MethodDeclaration") {
|
|
668
|
+
const sig = extractMethodSignature(prop, sourceFile);
|
|
669
|
+
if (sig) signatures.push(sig);
|
|
670
|
+
} else if (prop.getKindName() === "PropertyAssignment") {
|
|
671
|
+
const propertyAssignment = prop;
|
|
672
|
+
const initializer = propertyAssignment.getInitializer();
|
|
673
|
+
if (initializer) {
|
|
674
|
+
const kindName = initializer.getKindName();
|
|
675
|
+
if (kindName === "ArrowFunction" || kindName === "FunctionExpression") {
|
|
676
|
+
const name = propertyAssignment.getName();
|
|
677
|
+
const sig = extractFunctionSignature(
|
|
678
|
+
name,
|
|
679
|
+
initializer,
|
|
680
|
+
sourceFile
|
|
681
|
+
);
|
|
682
|
+
if (sig) signatures.push(sig);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return signatures;
|
|
688
|
+
}
|
|
689
|
+
function extractMethodSignature(method, sourceFile) {
|
|
690
|
+
const name = method.getName();
|
|
691
|
+
if (!name) return null;
|
|
692
|
+
const params = method.getParameters().map((p) => {
|
|
693
|
+
const typeNode = p.getTypeNode();
|
|
694
|
+
return {
|
|
695
|
+
name: p.getName(),
|
|
696
|
+
type: typeNode ? typeNode.getText() : "any",
|
|
697
|
+
optional: p.hasQuestionToken(),
|
|
698
|
+
rest: p.isRestParameter()
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
let returnType = "any";
|
|
702
|
+
const returnTypeNode = method.getReturnTypeNode();
|
|
703
|
+
if (returnTypeNode) {
|
|
704
|
+
returnType = resolveReturnType(returnTypeNode.getText());
|
|
705
|
+
} else {
|
|
706
|
+
const returnTypeFromChecker = method.getReturnType();
|
|
707
|
+
if (returnTypeFromChecker) {
|
|
708
|
+
returnType = resolveReturnType(returnTypeFromChecker.getText());
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return { name, params, returnType };
|
|
712
|
+
}
|
|
713
|
+
function extractFunctionSignature(name, fn, sourceFile) {
|
|
714
|
+
const params = fn.getParameters().map((p) => {
|
|
715
|
+
const typeNode = p.getTypeNode();
|
|
716
|
+
return {
|
|
717
|
+
name: p.getName(),
|
|
718
|
+
type: typeNode ? typeNode.getText() : "any",
|
|
719
|
+
optional: p.hasQuestionToken(),
|
|
720
|
+
rest: p.isRestParameter()
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
let returnType = "any";
|
|
724
|
+
const returnTypeNode = fn.getReturnTypeNode();
|
|
725
|
+
if (returnTypeNode) {
|
|
726
|
+
returnType = resolveReturnType(returnTypeNode.getText());
|
|
727
|
+
} else {
|
|
728
|
+
const returnTypeFromChecker = fn.getReturnType();
|
|
729
|
+
if (returnTypeFromChecker) {
|
|
730
|
+
returnType = resolveReturnType(returnTypeFromChecker.getText());
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return { name, params, returnType };
|
|
734
|
+
}
|
|
735
|
+
function resolveReturnType(raw) {
|
|
736
|
+
if (raw.startsWith("Promise<")) return raw;
|
|
737
|
+
return `Promise<${raw}>`;
|
|
738
|
+
}
|
|
739
|
+
function formatActionSignature(sig) {
|
|
740
|
+
const params = sig.params.map((p) => {
|
|
741
|
+
const rest = p.rest ? "..." : "";
|
|
742
|
+
const optional = p.optional ? "?" : "";
|
|
743
|
+
return `${rest}${p.name}${optional}: ${p.type}`;
|
|
744
|
+
}).join(", ");
|
|
745
|
+
return ` ${sig.name}: (${params}) => ${sig.returnType};`;
|
|
746
|
+
}
|
|
747
|
+
async function parseAllControllers(projectRoot, onWarn) {
|
|
748
|
+
const srcDir = path3.join(projectRoot, "src");
|
|
749
|
+
const controllersDir = path3.join(srcDir, "controllers");
|
|
750
|
+
if (!fs2.existsSync(controllersDir)) {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
const controllerFiles = fs2.readdirSync(controllersDir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
754
|
+
if (controllerFiles.length === 0) {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
const controllerInfos = [];
|
|
758
|
+
const project = new Project();
|
|
759
|
+
for (const file of controllerFiles) {
|
|
760
|
+
const filePath = path3.join(controllersDir, file);
|
|
761
|
+
try {
|
|
762
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
763
|
+
const classes = sourceFile.getClasses();
|
|
764
|
+
for (const classDecl of classes) {
|
|
765
|
+
const className = classDecl.getName();
|
|
766
|
+
if (!className) continue;
|
|
767
|
+
const extendsController = classDecl.getExtends()?.getText().includes("Controller") ?? false;
|
|
768
|
+
if (!extendsController) continue;
|
|
769
|
+
const actionsProperty = classDecl.getProperty("actions");
|
|
770
|
+
if (!actionsProperty) continue;
|
|
771
|
+
const initializer = actionsProperty.getInitializer();
|
|
772
|
+
if (!initializer || !initializer.getKindName().includes("ObjectLiteral")) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const actionsObject = initializer;
|
|
776
|
+
const actions = extractActionsType(actionsObject, sourceFile);
|
|
777
|
+
if (actions.length === 0) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
controllerInfos.push({
|
|
781
|
+
name: className,
|
|
782
|
+
filePath,
|
|
783
|
+
actions
|
|
784
|
+
});
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
const msg = `Failed to parse Controller in ${filePath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
789
|
+
onWarn?.(msg);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return controllerInfos;
|
|
793
|
+
}
|
|
794
|
+
async function generateTypesForInjection(projectRoot, onWarn) {
|
|
795
|
+
const controllerInfos = await parseAllControllers(projectRoot, onWarn);
|
|
796
|
+
if (controllerInfos.length === 0) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const lines = [];
|
|
800
|
+
lines.push("// Auto-generated bridge types - injected by MN Rails");
|
|
801
|
+
lines.push("// Do not edit this section manually");
|
|
802
|
+
lines.push("");
|
|
803
|
+
lines.push("interface BridgeActions {");
|
|
804
|
+
for (const info of controllerInfos) {
|
|
805
|
+
lines.push(` ${info.name}: {`);
|
|
806
|
+
for (const sig of info.actions) {
|
|
807
|
+
lines.push(formatActionSignature(sig));
|
|
808
|
+
}
|
|
809
|
+
lines.push(" };");
|
|
810
|
+
}
|
|
811
|
+
lines.push("}");
|
|
812
|
+
lines.push("");
|
|
813
|
+
lines.push("declare const bridge: BridgeActions;");
|
|
814
|
+
lines.push("");
|
|
815
|
+
return lines.join("\n");
|
|
816
|
+
}
|
|
817
|
+
async function generateTypes(options) {
|
|
818
|
+
const { projectRoot, outputPath, onWarn } = options;
|
|
819
|
+
const controllerInfos = await parseAllControllers(projectRoot, onWarn);
|
|
820
|
+
if (controllerInfos.length === 0) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const lines = [];
|
|
824
|
+
lines.push("// Auto-generated bridge types");
|
|
825
|
+
lines.push("// Do not edit this file manually");
|
|
826
|
+
lines.push("");
|
|
827
|
+
lines.push("export interface BridgeActions {");
|
|
828
|
+
for (const info of controllerInfos) {
|
|
829
|
+
lines.push(` ${info.name}: {`);
|
|
830
|
+
for (const sig of info.actions) {
|
|
831
|
+
lines.push(formatActionSignature(sig));
|
|
832
|
+
}
|
|
833
|
+
lines.push(" };");
|
|
834
|
+
}
|
|
835
|
+
lines.push("}");
|
|
836
|
+
lines.push("");
|
|
837
|
+
lines.push("declare global {");
|
|
838
|
+
lines.push(" const bridge: BridgeActions;");
|
|
839
|
+
lines.push("}");
|
|
840
|
+
lines.push("");
|
|
841
|
+
const output = outputPath || path3.join(projectRoot, "src", "bridge-types.ts");
|
|
842
|
+
await fs2.ensureDir(path3.dirname(output));
|
|
843
|
+
await fs2.writeFile(output, lines.join("\n"), "utf-8");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/build/index.ts
|
|
847
|
+
async function buildPlugin(projectRoot, options = {}) {
|
|
848
|
+
const srcDir = path4.join(projectRoot, "src");
|
|
849
|
+
const distDir = path4.join(projectRoot, "dist");
|
|
850
|
+
if (!fs3.existsSync(srcDir)) {
|
|
851
|
+
throw new Error("src directory not found");
|
|
852
|
+
}
|
|
853
|
+
if (fs3.existsSync(distDir)) {
|
|
854
|
+
fs3.removeSync(distDir);
|
|
855
|
+
}
|
|
856
|
+
let typeDefinitionsForInjection = null;
|
|
857
|
+
try {
|
|
858
|
+
await generateTypes({
|
|
859
|
+
projectRoot,
|
|
860
|
+
onWarn: (msg) => console.warn(chalk2.yellow(`Type gen: ${msg}`))
|
|
861
|
+
});
|
|
862
|
+
typeDefinitionsForInjection = await generateTypesForInjection(
|
|
863
|
+
projectRoot,
|
|
864
|
+
(msg) => console.warn(chalk2.yellow(`Type gen: ${msg}`))
|
|
865
|
+
);
|
|
866
|
+
console.log(chalk2.blue("\u2713 Bridge types generated"));
|
|
867
|
+
} catch (error) {
|
|
868
|
+
console.warn(chalk2.yellow(`Warning: Failed to generate types: ${error}`));
|
|
869
|
+
}
|
|
870
|
+
console.log(chalk2.blue("Compiling TypeScript..."));
|
|
871
|
+
try {
|
|
872
|
+
execSync("tsc", {
|
|
873
|
+
cwd: projectRoot,
|
|
874
|
+
stdio: "inherit"
|
|
875
|
+
});
|
|
876
|
+
} catch (error) {
|
|
877
|
+
throw new Error("TypeScript compilation failed");
|
|
878
|
+
}
|
|
879
|
+
const config = await loadConfig(projectRoot);
|
|
880
|
+
const addonDir = path4.join(projectRoot, "dist", "addon");
|
|
881
|
+
fs3.ensureDirSync(addonDir);
|
|
882
|
+
const copyFiles = (src, dest) => {
|
|
883
|
+
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
884
|
+
for (const entry of entries) {
|
|
885
|
+
const srcPath = path4.join(src, entry.name);
|
|
886
|
+
const destPath = path4.join(dest, entry.name);
|
|
887
|
+
if (entry.isDirectory()) {
|
|
888
|
+
fs3.ensureDirSync(destPath);
|
|
889
|
+
copyFiles(srcPath, destPath);
|
|
890
|
+
} else if (entry.name.endsWith(".js") || entry.name.endsWith(".d.ts")) {
|
|
891
|
+
fs3.copySync(srcPath, destPath);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const collectFilesByExt = (dir, ext, base = "") => {
|
|
896
|
+
const results = [];
|
|
897
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
898
|
+
for (const e of entries) {
|
|
899
|
+
const rel = base ? `${base}/${e.name}` : e.name;
|
|
900
|
+
if (e.isDirectory()) {
|
|
901
|
+
results.push(...collectFilesByExt(path4.join(dir, e.name), ext, rel));
|
|
902
|
+
} else if (e.name.endsWith(ext)) {
|
|
903
|
+
results.push(rel);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return results;
|
|
907
|
+
};
|
|
908
|
+
const copyViewsExcept = (src, dest, skipExts) => {
|
|
909
|
+
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
const srcPath = path4.join(src, entry.name);
|
|
912
|
+
const destPath = path4.join(dest, entry.name);
|
|
913
|
+
if (entry.isDirectory()) {
|
|
914
|
+
fs3.ensureDirSync(destPath);
|
|
915
|
+
copyViewsExcept(srcPath, destPath, skipExts);
|
|
916
|
+
} else {
|
|
917
|
+
const skip = skipExts.some((ext) => entry.name.endsWith(ext));
|
|
918
|
+
if (!skip) {
|
|
919
|
+
fs3.copySync(srcPath, destPath);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
copyFiles(distDir, addonDir);
|
|
925
|
+
const srcViewsDir = path4.join(projectRoot, "src", "views");
|
|
926
|
+
if (fs3.existsSync(srcViewsDir)) {
|
|
927
|
+
const addonViewsDir = path4.join(addonDir, "views");
|
|
928
|
+
fs3.ensureDirSync(addonViewsDir);
|
|
929
|
+
let typeDefinitions = null;
|
|
930
|
+
try {
|
|
931
|
+
typeDefinitions = await generateTypesForInjection(
|
|
932
|
+
projectRoot,
|
|
933
|
+
(msg) => console.warn(chalk2.yellow(`Type gen: ${msg}`))
|
|
934
|
+
);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.warn(chalk2.yellow(`Warning: Failed to generate types for injection: ${error}`));
|
|
937
|
+
}
|
|
938
|
+
const jsxFiles = collectFilesByExt(srcViewsDir, ".jsx");
|
|
939
|
+
if (jsxFiles.length > 0) {
|
|
940
|
+
const esbuild = await import("esbuild");
|
|
941
|
+
for (const rel of jsxFiles) {
|
|
942
|
+
const inputPath = path4.join(srcViewsDir, rel);
|
|
943
|
+
const outRel = rel.replace(/\.jsx$/, ".js");
|
|
944
|
+
const outputPath = path4.join(addonViewsDir, outRel);
|
|
945
|
+
fs3.ensureDirSync(path4.dirname(outputPath));
|
|
946
|
+
await esbuild.build({
|
|
947
|
+
entryPoints: [inputPath],
|
|
948
|
+
bundle: false,
|
|
949
|
+
format: "iife",
|
|
950
|
+
target: "es2015",
|
|
951
|
+
outfile: outputPath,
|
|
952
|
+
jsx: "transform"
|
|
953
|
+
});
|
|
954
|
+
if (typeDefinitions) {
|
|
955
|
+
let content = fs3.readFileSync(outputPath, "utf-8");
|
|
956
|
+
const typeComment = `/**
|
|
957
|
+
* @typedef {Object} BridgeActions
|
|
958
|
+
${typeDefinitions.split("\n").filter((l) => l.trim() && !l.startsWith("//")).map((l) => ` * ${l}`).join("\n")}
|
|
959
|
+
*/
|
|
960
|
+
`;
|
|
961
|
+
content = typeComment + content;
|
|
962
|
+
fs3.writeFileSync(outputPath, content, "utf-8");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
console.log(chalk2.blue(`\u2713 Compiled ${jsxFiles.length} JSX file(s)`));
|
|
966
|
+
}
|
|
967
|
+
const vueFiles = collectFilesByExt(srcViewsDir, ".vue");
|
|
968
|
+
if (vueFiles.length > 0) {
|
|
969
|
+
const { compileTemplate, compileScript, parse } = await import("@vue/compiler-sfc");
|
|
970
|
+
const esbuild = await import("esbuild");
|
|
971
|
+
for (const rel of vueFiles) {
|
|
972
|
+
const inputPath = path4.join(srcViewsDir, rel);
|
|
973
|
+
const outRel = rel.replace(/\.vue$/, ".js");
|
|
974
|
+
const outputPath = path4.join(addonViewsDir, outRel);
|
|
975
|
+
fs3.ensureDirSync(path4.dirname(outputPath));
|
|
976
|
+
try {
|
|
977
|
+
const source = fs3.readFileSync(inputPath, "utf-8");
|
|
978
|
+
const { descriptor, errors } = parse(source, { filename: inputPath });
|
|
979
|
+
if (errors.length > 0) {
|
|
980
|
+
console.warn(chalk2.yellow(`Vue parse warnings for ${rel}:`));
|
|
981
|
+
errors.forEach((err) => console.warn(chalk2.yellow(` - ${err.message}`)));
|
|
982
|
+
}
|
|
983
|
+
const componentId = `vue-${rel.replace(/[^a-z0-9]/gi, "-")}`;
|
|
984
|
+
let scriptCode = "";
|
|
985
|
+
if (descriptor.script || descriptor.scriptSetup) {
|
|
986
|
+
try {
|
|
987
|
+
const compiled = compileScript(descriptor, {
|
|
988
|
+
id: componentId,
|
|
989
|
+
inlineTemplate: !!descriptor.template
|
|
990
|
+
});
|
|
991
|
+
scriptCode = compiled.content;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.warn(chalk2.yellow(`Failed to compile script for ${rel}: ${error instanceof Error ? error.message : String(error)}`));
|
|
994
|
+
scriptCode = "export default {};";
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
scriptCode = "export default {};";
|
|
998
|
+
}
|
|
999
|
+
if (descriptor.template && !descriptor.scriptSetup) {
|
|
1000
|
+
try {
|
|
1001
|
+
const templateCompiled = compileTemplate({
|
|
1002
|
+
source: descriptor.template.content,
|
|
1003
|
+
filename: inputPath,
|
|
1004
|
+
id: componentId,
|
|
1005
|
+
compilerOptions: {
|
|
1006
|
+
mode: "module"
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
if (templateCompiled.code) {
|
|
1010
|
+
scriptCode = scriptCode.replace(
|
|
1011
|
+
/export\s+default\s+\{/,
|
|
1012
|
+
`const render = ${templateCompiled.code};
|
|
1013
|
+
export default { render,`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.warn(chalk2.yellow(`Failed to compile template for ${rel}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (descriptor.styles && descriptor.styles.length > 0) {
|
|
1021
|
+
const styleContent = descriptor.styles.map((style) => style.content).join("\n");
|
|
1022
|
+
const stylePath = outputPath.replace(/\.js$/, ".css");
|
|
1023
|
+
fs3.writeFileSync(stylePath, styleContent, "utf-8");
|
|
1024
|
+
}
|
|
1025
|
+
const result = await esbuild.transform(scriptCode, {
|
|
1026
|
+
loader: descriptor.script?.lang === "ts" || descriptor.scriptSetup?.lang === "ts" ? "ts" : "js",
|
|
1027
|
+
target: "es2015",
|
|
1028
|
+
format: "iife"
|
|
1029
|
+
});
|
|
1030
|
+
let compiledCode = result.code;
|
|
1031
|
+
if (typeDefinitions) {
|
|
1032
|
+
const typeComment = `/**
|
|
1033
|
+
* @typedef {Object} BridgeActions
|
|
1034
|
+
${typeDefinitions.split("\n").filter((l) => l.trim() && !l.startsWith("//")).map((l) => ` * ${l}`).join("\n")}
|
|
1035
|
+
*/
|
|
1036
|
+
`;
|
|
1037
|
+
compiledCode = typeComment + compiledCode;
|
|
1038
|
+
}
|
|
1039
|
+
fs3.writeFileSync(outputPath, compiledCode, "utf-8");
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.error(chalk2.red(`Failed to compile Vue file ${rel}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
console.log(chalk2.blue(`\u2713 Compiled ${vueFiles.length} Vue file(s)`));
|
|
1045
|
+
}
|
|
1046
|
+
const injectTypesIntoFile = (srcPath, destPath) => {
|
|
1047
|
+
if (srcPath.endsWith(".vue") || srcPath.endsWith(".jsx")) {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`Source file detected: ${srcPath}. Source files (.vue, .jsx) should not be copied to output directory. They must be compiled first. Please check your build configuration.`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (srcPath.endsWith(".js") || srcPath.endsWith(".ts")) {
|
|
1053
|
+
let content = fs3.readFileSync(srcPath, "utf-8");
|
|
1054
|
+
if (srcPath.endsWith(".ts") && typeDefinitions) {
|
|
1055
|
+
content = typeDefinitions + "\n\n" + content;
|
|
1056
|
+
} else if (srcPath.endsWith(".js") && typeDefinitions) {
|
|
1057
|
+
const typeComment = `/**
|
|
1058
|
+
* @typedef {Object} BridgeActions
|
|
1059
|
+
${typeDefinitions.split("\n").filter((l) => l.trim() && !l.startsWith("//")).map((l) => ` * ${l}`).join("\n")}
|
|
1060
|
+
*/
|
|
1061
|
+
`;
|
|
1062
|
+
content = typeComment + content;
|
|
1063
|
+
}
|
|
1064
|
+
fs3.writeFileSync(destPath, content, "utf-8");
|
|
1065
|
+
} else {
|
|
1066
|
+
fs3.copySync(srcPath, destPath);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
const copyViewsWithTypeInjection = (src, dest, skipExts) => {
|
|
1070
|
+
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
1071
|
+
for (const entry of entries) {
|
|
1072
|
+
const srcPath = path4.join(src, entry.name);
|
|
1073
|
+
const destPath = path4.join(dest, entry.name);
|
|
1074
|
+
if (entry.isDirectory()) {
|
|
1075
|
+
fs3.ensureDirSync(destPath);
|
|
1076
|
+
copyViewsWithTypeInjection(srcPath, destPath, skipExts);
|
|
1077
|
+
} else {
|
|
1078
|
+
const skip = skipExts.some((ext) => entry.name.endsWith(ext));
|
|
1079
|
+
if (!skip) {
|
|
1080
|
+
injectTypesIntoFile(srcPath, destPath);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
copyViewsWithTypeInjection(srcViewsDir, addonViewsDir, [".jsx", ".vue"]);
|
|
1086
|
+
console.log(chalk2.blue("\u2713 Views copied"));
|
|
1087
|
+
}
|
|
1088
|
+
const indexFile = path4.join(addonDir, "index.js");
|
|
1089
|
+
const mainFile = path4.join(addonDir, "main.js");
|
|
1090
|
+
if (fs3.existsSync(indexFile)) {
|
|
1091
|
+
let mainContent = fs3.readFileSync(indexFile, "utf-8");
|
|
1092
|
+
if (options.devMode && options.devServerUrl) {
|
|
1093
|
+
const companionCode = generateCompanionCode({
|
|
1094
|
+
serverUrl: options.devServerUrl
|
|
1095
|
+
});
|
|
1096
|
+
mainContent = companionCode + "\n" + mainContent;
|
|
1097
|
+
console.log(chalk2.blue("\u2713 Debug companion injected"));
|
|
1098
|
+
}
|
|
1099
|
+
fs3.writeFileSync(mainFile, mainContent);
|
|
1100
|
+
fs3.removeSync(indexFile);
|
|
1101
|
+
}
|
|
1102
|
+
if (typeDefinitionsForInjection) {
|
|
1103
|
+
const typesFile = path4.join(addonDir, "bridge-types.js");
|
|
1104
|
+
const jsdocTypes = typeDefinitionsForInjection.split("\n").filter((line) => line.trim() && !line.startsWith("//")).map((line) => ` * ${line}`).join("\n");
|
|
1105
|
+
const typesCode = `/**
|
|
1106
|
+
* Auto-generated bridge types for runtime injection
|
|
1107
|
+
* @typedef {Object} BridgeActions
|
|
1108
|
+
${jsdocTypes}
|
|
1109
|
+
* @typedef {BridgeActions} bridge
|
|
1110
|
+
*/
|
|
1111
|
+
`;
|
|
1112
|
+
fs3.writeFileSync(typesFile, typesCode, "utf-8");
|
|
1113
|
+
}
|
|
1114
|
+
const addonJson = {
|
|
1115
|
+
key: config.name || path4.basename(projectRoot),
|
|
1116
|
+
title: config.title || config.name || path4.basename(projectRoot),
|
|
1117
|
+
version: config.version || "0.1.0",
|
|
1118
|
+
author: config.author || "",
|
|
1119
|
+
main: "main.js"
|
|
1120
|
+
};
|
|
1121
|
+
fs3.writeJSONSync(path4.join(addonDir, "addon.json"), addonJson, { spaces: 2 });
|
|
1122
|
+
const addonName = `${addonJson.key}-${addonJson.version}.mnaddon`;
|
|
1123
|
+
const addonPath = path4.join(projectRoot, addonName);
|
|
1124
|
+
try {
|
|
1125
|
+
execSync(`cd ${addonDir} && zip -r ${addonPath} .`, {
|
|
1126
|
+
stdio: "inherit"
|
|
1127
|
+
});
|
|
1128
|
+
console.log(chalk2.green(`\u2713 Plugin packaged: ${addonName}`));
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.warn(chalk2.yellow("\u26A0\uFE0F Could not create .mnaddon file (zip command not found)"));
|
|
1131
|
+
console.log(chalk2.cyan(` Plugin files are in: ${addonDir}`));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/commands/build.ts
|
|
1136
|
+
async function buildCommand(options = {}) {
|
|
1137
|
+
try {
|
|
1138
|
+
const projectRoot = process.cwd();
|
|
1139
|
+
const configPath = path5.join(projectRoot, "mn-rails.config.js");
|
|
1140
|
+
if (!fs4.existsSync(configPath)) {
|
|
1141
|
+
console.error(chalk3.red("Error: mn-rails.config.js not found. Are you in a plugin project?"));
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
console.log(chalk3.blue("Building plugin..."));
|
|
1145
|
+
await buildPlugin(projectRoot, {
|
|
1146
|
+
watch: options.watch || false
|
|
1147
|
+
});
|
|
1148
|
+
if (!options.watch) {
|
|
1149
|
+
console.log(chalk3.green("\u2713 Build completed successfully!"));
|
|
1150
|
+
}
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/commands/dev.ts
|
|
1158
|
+
import chalk7 from "chalk";
|
|
1159
|
+
import * as path8 from "path";
|
|
1160
|
+
|
|
1161
|
+
// src/dev/server.ts
|
|
1162
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1163
|
+
import chalk4 from "chalk";
|
|
1164
|
+
|
|
1165
|
+
// src/dev/protocol.ts
|
|
1166
|
+
function createMessage(type, payload, id) {
|
|
1167
|
+
return {
|
|
1168
|
+
type,
|
|
1169
|
+
payload,
|
|
1170
|
+
timestamp: Date.now(),
|
|
1171
|
+
id
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function parseMessage(data) {
|
|
1175
|
+
try {
|
|
1176
|
+
const message = JSON.parse(data);
|
|
1177
|
+
if (message.type && message.payload !== void 0) {
|
|
1178
|
+
return message;
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function serializeMessage(message) {
|
|
1186
|
+
return JSON.stringify(message);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/dev/server.ts
|
|
1190
|
+
var DevServer = class {
|
|
1191
|
+
wss = null;
|
|
1192
|
+
clients = /* @__PURE__ */ new Set();
|
|
1193
|
+
options;
|
|
1194
|
+
heartbeatInterval = null;
|
|
1195
|
+
constructor(options) {
|
|
1196
|
+
this.options = options;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 启动服务器
|
|
1200
|
+
*/
|
|
1201
|
+
start() {
|
|
1202
|
+
return new Promise((resolve2, reject) => {
|
|
1203
|
+
try {
|
|
1204
|
+
this.wss = new WebSocketServer({ port: this.options.port });
|
|
1205
|
+
this.wss.on("listening", () => {
|
|
1206
|
+
console.log(
|
|
1207
|
+
chalk4.green(`\u2713 Development server started on ws://localhost:${this.options.port}`)
|
|
1208
|
+
);
|
|
1209
|
+
this.startHeartbeat();
|
|
1210
|
+
resolve2();
|
|
1211
|
+
});
|
|
1212
|
+
this.wss.on("error", (error) => {
|
|
1213
|
+
console.error(chalk4.red(`WebSocket server error: ${error.message}`));
|
|
1214
|
+
reject(error);
|
|
1215
|
+
});
|
|
1216
|
+
this.wss.on("connection", (ws) => {
|
|
1217
|
+
this.handleConnection(ws);
|
|
1218
|
+
});
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
reject(error);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* 停止服务器
|
|
1226
|
+
*/
|
|
1227
|
+
stop() {
|
|
1228
|
+
return new Promise((resolve2) => {
|
|
1229
|
+
if (this.heartbeatInterval) {
|
|
1230
|
+
clearInterval(this.heartbeatInterval);
|
|
1231
|
+
this.heartbeatInterval = null;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.wss) {
|
|
1234
|
+
this.clients.forEach((ws) => {
|
|
1235
|
+
ws.close();
|
|
1236
|
+
});
|
|
1237
|
+
this.clients.clear();
|
|
1238
|
+
this.wss.close(() => {
|
|
1239
|
+
console.log(chalk4.yellow("Development server stopped"));
|
|
1240
|
+
resolve2();
|
|
1241
|
+
});
|
|
1242
|
+
this.wss = null;
|
|
1243
|
+
} else {
|
|
1244
|
+
resolve2();
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* 处理新连接
|
|
1250
|
+
*/
|
|
1251
|
+
handleConnection(ws) {
|
|
1252
|
+
this.clients.add(ws);
|
|
1253
|
+
console.log(chalk4.cyan(`Client connected (${this.clients.size} total)`));
|
|
1254
|
+
this.send(ws, createMessage("connected", { message: "Connected to dev server" }));
|
|
1255
|
+
if (this.options.onConnection) {
|
|
1256
|
+
this.options.onConnection(ws);
|
|
1257
|
+
}
|
|
1258
|
+
ws.on("message", (data) => {
|
|
1259
|
+
const message = parseMessage(data.toString());
|
|
1260
|
+
if (message) {
|
|
1261
|
+
this.handleMessage(ws, message);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
ws.on("close", () => {
|
|
1265
|
+
this.clients.delete(ws);
|
|
1266
|
+
console.log(chalk4.yellow(`Client disconnected (${this.clients.size} total)`));
|
|
1267
|
+
if (this.options.onDisconnection) {
|
|
1268
|
+
this.options.onDisconnection(ws);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
ws.on("error", (error) => {
|
|
1272
|
+
console.error(chalk4.red(`WebSocket error: ${error.message}`));
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* 处理消息
|
|
1277
|
+
*/
|
|
1278
|
+
handleMessage(ws, message) {
|
|
1279
|
+
if (message.type === "heartbeat") {
|
|
1280
|
+
this.send(ws, createMessage("heartbeat", { timestamp: Date.now() }));
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (message.type === "ready") {
|
|
1284
|
+
console.log(chalk4.green("Client ready"));
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (this.options.onMessage) {
|
|
1288
|
+
this.options.onMessage(ws, message);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 发送消息到指定客户端
|
|
1293
|
+
*/
|
|
1294
|
+
send(ws, message) {
|
|
1295
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1296
|
+
try {
|
|
1297
|
+
ws.send(serializeMessage(message));
|
|
1298
|
+
return true;
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
console.error(chalk4.red(`Failed to send message: ${error}`));
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* 广播消息到所有客户端
|
|
1308
|
+
*/
|
|
1309
|
+
broadcast(message) {
|
|
1310
|
+
let count = 0;
|
|
1311
|
+
this.clients.forEach((ws) => {
|
|
1312
|
+
if (this.send(ws, message)) {
|
|
1313
|
+
count++;
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
return count;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* 获取客户端数量
|
|
1320
|
+
*/
|
|
1321
|
+
getClientCount() {
|
|
1322
|
+
return this.clients.size;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* 启动心跳检测
|
|
1326
|
+
*/
|
|
1327
|
+
startHeartbeat() {
|
|
1328
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1329
|
+
this.clients.forEach((ws) => {
|
|
1330
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1331
|
+
this.send(ws, createMessage("heartbeat", { timestamp: Date.now() }));
|
|
1332
|
+
} else {
|
|
1333
|
+
this.clients.delete(ws);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
}, 3e4);
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// src/dev/watcher.ts
|
|
1341
|
+
import * as path6 from "path";
|
|
1342
|
+
import chokidar from "chokidar";
|
|
1343
|
+
import chalk5 from "chalk";
|
|
1344
|
+
var FileWatcher = class {
|
|
1345
|
+
watcher = null;
|
|
1346
|
+
options;
|
|
1347
|
+
constructor(options) {
|
|
1348
|
+
this.options = options;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* 开始监听
|
|
1352
|
+
*/
|
|
1353
|
+
start() {
|
|
1354
|
+
return new Promise((resolve2, reject) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const watchPaths = this.options.patterns.map(
|
|
1357
|
+
(pattern) => path6.join(this.options.rootDir, pattern)
|
|
1358
|
+
);
|
|
1359
|
+
const ignored = [
|
|
1360
|
+
"**/node_modules/**",
|
|
1361
|
+
"**/dist/**",
|
|
1362
|
+
"**/.git/**",
|
|
1363
|
+
"**/*.mnaddon",
|
|
1364
|
+
...this.options.ignored || []
|
|
1365
|
+
];
|
|
1366
|
+
this.watcher = chokidar.watch(watchPaths, {
|
|
1367
|
+
ignored,
|
|
1368
|
+
persistent: true,
|
|
1369
|
+
ignoreInitial: true,
|
|
1370
|
+
awaitWriteFinish: {
|
|
1371
|
+
stabilityThreshold: 200,
|
|
1372
|
+
pollInterval: 100
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
this.watcher.on("ready", () => {
|
|
1376
|
+
console.log(chalk5.blue("\u2713 File watcher started"));
|
|
1377
|
+
console.log(chalk5.gray(`Watching: ${watchPaths.join(", ")}`));
|
|
1378
|
+
resolve2();
|
|
1379
|
+
});
|
|
1380
|
+
this.watcher.on("change", (filePath) => {
|
|
1381
|
+
console.log(chalk5.cyan(`File changed: ${path6.relative(this.options.rootDir, filePath)}`));
|
|
1382
|
+
if (this.options.onFileChange) {
|
|
1383
|
+
this.options.onFileChange(filePath);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
this.watcher.on("error", (err) => {
|
|
1387
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1388
|
+
console.error(chalk5.red(`File watcher error: ${error.message}`));
|
|
1389
|
+
if (this.options.onError) {
|
|
1390
|
+
this.options.onError(error);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
this.watcher.on("add", (filePath) => {
|
|
1394
|
+
console.log(chalk5.gray(`File added: ${path6.relative(this.options.rootDir, filePath)}`));
|
|
1395
|
+
});
|
|
1396
|
+
this.watcher.on("unlink", (filePath) => {
|
|
1397
|
+
console.log(chalk5.gray(`File removed: ${path6.relative(this.options.rootDir, filePath)}`));
|
|
1398
|
+
});
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
reject(error);
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* 停止监听
|
|
1406
|
+
*/
|
|
1407
|
+
async stop() {
|
|
1408
|
+
if (this.watcher) {
|
|
1409
|
+
await this.watcher.close();
|
|
1410
|
+
this.watcher = null;
|
|
1411
|
+
console.log(chalk5.yellow("File watcher stopped"));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
// src/dev/builder.ts
|
|
1417
|
+
import chalk6 from "chalk";
|
|
1418
|
+
|
|
1419
|
+
// src/dev/code-loader.ts
|
|
1420
|
+
import * as path7 from "path";
|
|
1421
|
+
import * as fs5 from "fs-extra";
|
|
1422
|
+
function loadPluginCode(projectRoot) {
|
|
1423
|
+
try {
|
|
1424
|
+
const mainFile = path7.join(projectRoot, "dist", "addon", "main.js");
|
|
1425
|
+
if (!fs5.existsSync(mainFile)) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
let code = fs5.readFileSync(mainFile, "utf-8");
|
|
1429
|
+
const companionEndMarker = "})();";
|
|
1430
|
+
const companionIndex = code.indexOf(companionEndMarker);
|
|
1431
|
+
if (companionIndex !== -1) {
|
|
1432
|
+
code = code.substring(companionIndex + companionEndMarker.length).trim();
|
|
1433
|
+
}
|
|
1434
|
+
return code;
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
console.error(`Failed to load plugin code: ${error}`);
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/dev/builder.ts
|
|
1442
|
+
var DevBuilder = class {
|
|
1443
|
+
options;
|
|
1444
|
+
isBuilding = false;
|
|
1445
|
+
buildQueue = [];
|
|
1446
|
+
processingQueue = false;
|
|
1447
|
+
constructor(options) {
|
|
1448
|
+
this.options = options;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* 触发构建
|
|
1452
|
+
*/
|
|
1453
|
+
async build() {
|
|
1454
|
+
if (this.isBuilding) {
|
|
1455
|
+
return new Promise((resolve2) => {
|
|
1456
|
+
this.buildQueue.push(async () => {
|
|
1457
|
+
await this.doBuild();
|
|
1458
|
+
resolve2();
|
|
1459
|
+
});
|
|
1460
|
+
this.processQueue();
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return this.doBuild();
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* 执行构建
|
|
1467
|
+
*/
|
|
1468
|
+
async doBuild() {
|
|
1469
|
+
if (this.isBuilding) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
this.isBuilding = true;
|
|
1473
|
+
if (this.options.onBuildStart) {
|
|
1474
|
+
this.options.onBuildStart();
|
|
1475
|
+
}
|
|
1476
|
+
try {
|
|
1477
|
+
console.log(chalk6.blue("Building plugin..."));
|
|
1478
|
+
await buildPlugin(this.options.projectRoot, {
|
|
1479
|
+
watch: false,
|
|
1480
|
+
devMode: true,
|
|
1481
|
+
devServerUrl: this.options.devServerUrl
|
|
1482
|
+
});
|
|
1483
|
+
const code = loadPluginCode(this.options.projectRoot);
|
|
1484
|
+
if (this.options.onBuildComplete) {
|
|
1485
|
+
this.options.onBuildComplete(true, void 0, code || void 0);
|
|
1486
|
+
}
|
|
1487
|
+
console.log(chalk6.green("\u2713 Build completed"));
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1490
|
+
console.error(chalk6.red(`\u2717 Build failed: ${err.message}`));
|
|
1491
|
+
if (this.options.onBuildComplete) {
|
|
1492
|
+
this.options.onBuildComplete(false, err);
|
|
1493
|
+
}
|
|
1494
|
+
} finally {
|
|
1495
|
+
this.isBuilding = false;
|
|
1496
|
+
this.processQueue();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* 处理构建队列
|
|
1501
|
+
*/
|
|
1502
|
+
async processQueue() {
|
|
1503
|
+
if (this.processingQueue || this.buildQueue.length === 0) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
this.processingQueue = true;
|
|
1507
|
+
while (this.buildQueue.length > 0 && !this.isBuilding) {
|
|
1508
|
+
const task = this.buildQueue.shift();
|
|
1509
|
+
if (task) {
|
|
1510
|
+
await task();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
this.processingQueue = false;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* 检查是否正在构建
|
|
1517
|
+
*/
|
|
1518
|
+
get building() {
|
|
1519
|
+
return this.isBuilding;
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// src/dev/logger.ts
|
|
1524
|
+
import { createConsola } from "consola";
|
|
1525
|
+
var logger = createConsola({
|
|
1526
|
+
formatOptions: {
|
|
1527
|
+
date: true,
|
|
1528
|
+
colors: true,
|
|
1529
|
+
compact: false
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
function printLogMessage(message) {
|
|
1533
|
+
const { level, args } = message;
|
|
1534
|
+
const parsedArgs = args.map((arg) => {
|
|
1535
|
+
try {
|
|
1536
|
+
return JSON.parse(arg);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return arg;
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
switch (level) {
|
|
1542
|
+
case "error":
|
|
1543
|
+
if (parsedArgs.length > 0) {
|
|
1544
|
+
logger.error(...parsedArgs);
|
|
1545
|
+
} else {
|
|
1546
|
+
logger.error("");
|
|
1547
|
+
}
|
|
1548
|
+
break;
|
|
1549
|
+
case "warn":
|
|
1550
|
+
if (parsedArgs.length > 0) {
|
|
1551
|
+
logger.warn(...parsedArgs);
|
|
1552
|
+
} else {
|
|
1553
|
+
logger.warn("");
|
|
1554
|
+
}
|
|
1555
|
+
break;
|
|
1556
|
+
case "info":
|
|
1557
|
+
if (parsedArgs.length > 0) {
|
|
1558
|
+
logger.info(...parsedArgs);
|
|
1559
|
+
} else {
|
|
1560
|
+
logger.info("");
|
|
1561
|
+
}
|
|
1562
|
+
break;
|
|
1563
|
+
case "log":
|
|
1564
|
+
default:
|
|
1565
|
+
if (parsedArgs.length > 0) {
|
|
1566
|
+
logger.log(...parsedArgs);
|
|
1567
|
+
} else {
|
|
1568
|
+
logger.log("");
|
|
1569
|
+
}
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/commands/dev.ts
|
|
1575
|
+
async function devCommand(options = {}) {
|
|
1576
|
+
const port = parseInt(options.port || "3000", 10);
|
|
1577
|
+
const projectRoot = process.cwd();
|
|
1578
|
+
console.log(chalk7.blue("Starting development server..."));
|
|
1579
|
+
const configPath = path8.join(projectRoot, "mn-rails.config.js");
|
|
1580
|
+
try {
|
|
1581
|
+
const fs6 = await import("fs-extra");
|
|
1582
|
+
if (!fs6.existsSync(configPath)) {
|
|
1583
|
+
console.error(chalk7.red("Error: mn-rails.config.js not found. Are you in a plugin project?"));
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
console.error(chalk7.red(`Error checking project: ${error}`));
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
let isFirstBuild = true;
|
|
1591
|
+
const devServerUrl = `ws://localhost:${port}`;
|
|
1592
|
+
const builder = new DevBuilder({
|
|
1593
|
+
projectRoot,
|
|
1594
|
+
devServerUrl,
|
|
1595
|
+
onBuildStart: () => {
|
|
1596
|
+
if (!isFirstBuild) {
|
|
1597
|
+
console.log(chalk7.blue("Rebuilding..."));
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
onBuildComplete: async (success, error, code) => {
|
|
1601
|
+
if (success && code) {
|
|
1602
|
+
const updateMessage = {
|
|
1603
|
+
type: "code-update",
|
|
1604
|
+
payload: {
|
|
1605
|
+
message: "Build completed",
|
|
1606
|
+
code
|
|
1607
|
+
},
|
|
1608
|
+
timestamp: Date.now()
|
|
1609
|
+
};
|
|
1610
|
+
const count = server.broadcast(updateMessage);
|
|
1611
|
+
if (count > 0) {
|
|
1612
|
+
console.log(chalk7.green(`\u2713 Code update sent to ${count} client(s)`));
|
|
1613
|
+
}
|
|
1614
|
+
if (isFirstBuild) {
|
|
1615
|
+
isFirstBuild = false;
|
|
1616
|
+
const fs6 = await import("fs-extra");
|
|
1617
|
+
const addonDir = path8.join(projectRoot, "dist", "addon");
|
|
1618
|
+
const addonJsonPath = path8.join(addonDir, "addon.json");
|
|
1619
|
+
if (fs6.existsSync(addonJsonPath)) {
|
|
1620
|
+
const addonJson = fs6.readJSONSync(addonJsonPath);
|
|
1621
|
+
const addonName = `${addonJson.key}-${addonJson.version}.mnaddon`;
|
|
1622
|
+
const addonPath = path8.join(projectRoot, addonName);
|
|
1623
|
+
console.log(chalk7.green("\n\u2713 Initial build completed!"));
|
|
1624
|
+
console.log(chalk7.cyan("\n\u{1F4E6} Plugin Information:"));
|
|
1625
|
+
console.log(chalk7.white(` Name: ${addonJson.title || addonJson.key}`));
|
|
1626
|
+
console.log(chalk7.white(` Version: ${addonJson.version}`));
|
|
1627
|
+
if (fs6.existsSync(addonPath)) {
|
|
1628
|
+
console.log(chalk7.cyan(`
|
|
1629
|
+
\u{1F4C1} Plugin file: ${addonPath}`));
|
|
1630
|
+
} else {
|
|
1631
|
+
console.log(chalk7.cyan(`
|
|
1632
|
+
\u{1F4C1} Plugin directory: ${addonDir}`));
|
|
1633
|
+
}
|
|
1634
|
+
console.log(chalk7.cyan("\n\u{1F4CB} Installation Instructions:"));
|
|
1635
|
+
console.log(chalk7.white(" 1. Open MarginNote app"));
|
|
1636
|
+
console.log(chalk7.white(" 2. Go to Settings > Add-ons"));
|
|
1637
|
+
console.log(chalk7.white(' 3. Tap "Import Add-on" or drag the .mnaddon file'));
|
|
1638
|
+
if (fs6.existsSync(addonPath)) {
|
|
1639
|
+
console.log(chalk7.white(` 4. Select: ${addonName}`));
|
|
1640
|
+
} else {
|
|
1641
|
+
console.log(chalk7.white(` 4. Select the addon directory: ${addonDir}`));
|
|
1642
|
+
}
|
|
1643
|
+
console.log(chalk7.cyan("\n\u{1F50C} Connection:"));
|
|
1644
|
+
console.log(chalk7.white(` WebSocket server: ${devServerUrl}`));
|
|
1645
|
+
console.log(chalk7.white(" Waiting for plugin to connect..."));
|
|
1646
|
+
console.log(chalk7.gray("\n The plugin will automatically connect when MarginNote is opened."));
|
|
1647
|
+
console.log(chalk7.gray(" Hot reload will work once connected.\n"));
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
} else if (error) {
|
|
1651
|
+
console.error(chalk7.red(`
|
|
1652
|
+
\u2717 Build failed: ${error.message}`));
|
|
1653
|
+
if (isFirstBuild) {
|
|
1654
|
+
console.log(chalk7.yellow("\n\u{1F4A1} Troubleshooting:"));
|
|
1655
|
+
console.log(chalk7.white(" - Check if TypeScript compilation errors exist"));
|
|
1656
|
+
console.log(chalk7.white(" - Ensure all dependencies are installed (npm install)"));
|
|
1657
|
+
console.log(chalk7.white(" - Verify mn-rails.config.js is properly configured\n"));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
const watcher = new FileWatcher({
|
|
1663
|
+
rootDir: projectRoot,
|
|
1664
|
+
patterns: ["src/**/*"],
|
|
1665
|
+
onFileChange: async (filePath) => {
|
|
1666
|
+
await builder.build();
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
const server = new DevServer({
|
|
1670
|
+
port,
|
|
1671
|
+
onConnection: (ws) => {
|
|
1672
|
+
const count = server.getClientCount();
|
|
1673
|
+
console.log(chalk7.green(`\u2713 Client connected (${count} total)`));
|
|
1674
|
+
},
|
|
1675
|
+
onDisconnection: (ws) => {
|
|
1676
|
+
const count = server.getClientCount();
|
|
1677
|
+
console.log(chalk7.yellow(`Client disconnected (${count} remaining)`));
|
|
1678
|
+
},
|
|
1679
|
+
onMessage: (ws, message) => {
|
|
1680
|
+
if (message.type === "log" || message.type === "error" || message.type === "warn") {
|
|
1681
|
+
const logMessage = {
|
|
1682
|
+
level: message.type,
|
|
1683
|
+
args: message.payload.args || [message.payload],
|
|
1684
|
+
timestamp: message.timestamp
|
|
1685
|
+
};
|
|
1686
|
+
printLogMessage(logMessage);
|
|
1687
|
+
} else if (message.type === "ready") {
|
|
1688
|
+
const count = server.getClientCount();
|
|
1689
|
+
console.log(chalk7.green(`\u2713 Client ready (${count} connected)`));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
try {
|
|
1694
|
+
await server.start();
|
|
1695
|
+
await watcher.start();
|
|
1696
|
+
console.log(chalk7.blue("Performing initial build..."));
|
|
1697
|
+
let buildSuccess = false;
|
|
1698
|
+
try {
|
|
1699
|
+
await builder.build();
|
|
1700
|
+
buildSuccess = true;
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
console.error(chalk7.red(`
|
|
1703
|
+
\u2717 Initial build failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1704
|
+
console.log(chalk7.yellow("\n\u{1F4A1} Troubleshooting:"));
|
|
1705
|
+
console.log(chalk7.white(" - Check if TypeScript compilation errors exist"));
|
|
1706
|
+
console.log(chalk7.white(" - Ensure all dependencies are installed (npm install)"));
|
|
1707
|
+
console.log(chalk7.white(" - Verify mn-rails.config.js is properly configured"));
|
|
1708
|
+
console.log(chalk7.white(" - Check that src/ directory exists and contains valid code\n"));
|
|
1709
|
+
}
|
|
1710
|
+
if (buildSuccess) {
|
|
1711
|
+
console.log(chalk7.green("\n\u2713 Development server ready!"));
|
|
1712
|
+
console.log(chalk7.cyan("\n\u{1F4CA} Status:"));
|
|
1713
|
+
console.log(chalk7.white(` Server: ws://localhost:${port}`));
|
|
1714
|
+
console.log(chalk7.white(` Clients: ${server.getClientCount()}`));
|
|
1715
|
+
console.log(chalk7.cyan("\n\u23F3 Waiting for file changes...\n"));
|
|
1716
|
+
}
|
|
1717
|
+
const shutdown = async () => {
|
|
1718
|
+
console.log(chalk7.yellow("\nShutting down development server..."));
|
|
1719
|
+
await watcher.stop();
|
|
1720
|
+
await server.stop();
|
|
1721
|
+
process.exit(0);
|
|
1722
|
+
};
|
|
1723
|
+
process.on("SIGINT", shutdown);
|
|
1724
|
+
process.on("SIGTERM", shutdown);
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
console.error(chalk7.red(`Failed to start server: ${error}`));
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/index.ts
|
|
1732
|
+
var program = new Command();
|
|
1733
|
+
program.name("mn-rails").description("A modern plugin development framework for MarginNote").version("0.1.0");
|
|
1734
|
+
program.command("new").description("Create a new MarginNote plugin project").argument("[name]", "Project name").option("-t, --template <template>", "Template to use", "basic").action(newCommand);
|
|
1735
|
+
program.command("build").description("Build the plugin for production").option("-w, --watch", "Watch mode").action(buildCommand);
|
|
1736
|
+
program.command("dev").description("Start development server with hot reload").option("-p, --port <port>", "Server port", "3000").action(devCommand);
|
|
1737
|
+
program.parse(process.argv);
|