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/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);