@majkapp/plugin-kit 1.0.7 → 1.0.9

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.
@@ -33,6 +33,18 @@ export interface FluentBuilder<Id extends string> {
33
33
  onReady(fn: (ctx: PluginContext, cleanup: (fn: CleanupFn) => void) => void | Promise<void>): this;
34
34
  /** Custom health check */
35
35
  health(fn: HealthCheckFn): this;
36
+ /**
37
+ * Set the plugin root directory explicitly (use __dirname from your plugin entry file).
38
+ * This ensures plugin-kit can find your files in both local development and npm installed scenarios.
39
+ * If not provided, falls back to the pluginRoot from MAJK's context.
40
+ *
41
+ * @example
42
+ * const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
43
+ * .pluginRoot(__dirname) // Use Node's __dirname
44
+ * .ui({ appDir: 'dist' }) // Now resolves relative to __dirname
45
+ * .build();
46
+ */
47
+ pluginRoot(dir: string): this;
36
48
  /** Build the plugin */
37
49
  build(): InProcessPlugin;
38
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-kit.d.ts","sourceRoot":"","sources":["../src/plugin-kit.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EAGb,eAAe,EACf,QAAQ,EACR,WAAW,EACX,WAAW,EACX,QAAQ,EACR,WAAW,EACX,UAAU,EACV,eAAe,EACf,WAAW,EACX,KAAK,EACL,UAAU,EACV,SAAS,EACT,aAAa,EAIb,eAAe,EACf,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAgqBjB;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,EAAE,SAAS,MAAM;IAC9C,mDAAmD;IACnD,MAAM,CAAC,KAAK,EAAE,mBAAmB,EAAE,IAAI,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAE7G,0CAA0C;IAC1C,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAE5B,yBAAyB;IACzB,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAE3C,yBAAyB;IACzB,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAEzC,uBAAuB;IACvB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAEnC,iBAAiB;IACjB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAE/D,oEAAoE;IACpE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAEtD,8DAA8D;IAC9D,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAE5C,+DAA+D;IAC/D,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC;IAE9C,wBAAwB;IACxB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAC;IAEzC,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAC;IAEjC,sCAAsC;IACtC,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAElG,0BAA0B;IAC1B,MAAM,CAAC,EAAE,EAAE,aAAa,GAAG,IAAI,CAAC;IAEhC,uBAAuB;IACvB,KAAK,IAAI,eAAe,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAClD,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,aAAa,CAAC,EAAE,CAAC,CAoUnB"}
1
+ {"version":3,"file":"plugin-kit.d.ts","sourceRoot":"","sources":["../src/plugin-kit.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EAGb,eAAe,EACf,QAAQ,EACR,WAAW,EACX,WAAW,EACX,QAAQ,EACR,WAAW,EACX,UAAU,EACV,eAAe,EACf,WAAW,EACX,KAAK,EACL,UAAU,EACV,SAAS,EACT,aAAa,EAIb,eAAe,EACf,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAwsBjB;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,EAAE,SAAS,MAAM;IAC9C,mDAAmD;IACnD,MAAM,CAAC,KAAK,EAAE,mBAAmB,EAAE,IAAI,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAE7G,0CAA0C;IAC1C,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAE5B,yBAAyB;IACzB,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAE3C,yBAAyB;IACzB,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAEzC,uBAAuB;IACvB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAEnC,iBAAiB;IACjB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAE/D,oEAAoE;IACpE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAEtD,8DAA8D;IAC9D,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAE5C,+DAA+D;IAC/D,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC;IAE9C,wBAAwB;IACxB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAC;IAEzC,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAC;IAEjC,sCAAsC;IACtC,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAElG,0BAA0B;IAC1B,MAAM,CAAC,EAAE,EAAE,aAAa,GAAG,IAAI,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B,uBAAuB;IACvB,KAAK,IAAI,eAAe,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAClD,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,aAAa,CAAC,EAAE,CAAC,CAkVnB"}
@@ -286,7 +286,7 @@ function serveSpa(ui, req, res, ctx) {
286
286
  * Main plugin class built by the fluent API
287
287
  */
288
288
  class BuiltPlugin {
289
- constructor(id, name, version, capabilities, tools, apiRoutes, uiConfig, reactScreens, htmlScreens, wizard, onReadyFn, healthCheckFn) {
289
+ constructor(id, name, version, capabilities, tools, apiRoutes, uiConfig, reactScreens, htmlScreens, wizard, onReadyFn, healthCheckFn, userProvidedPluginRoot) {
290
290
  this.capabilities = capabilities;
291
291
  this.tools = tools;
292
292
  this.apiRoutes = apiRoutes;
@@ -296,6 +296,7 @@ class BuiltPlugin {
296
296
  this.wizard = wizard;
297
297
  this.onReadyFn = onReadyFn;
298
298
  this.healthCheckFn = healthCheckFn;
299
+ this.userProvidedPluginRoot = userProvidedPluginRoot;
299
300
  this.cleanups = [];
300
301
  this.router = [];
301
302
  this.id = id;
@@ -334,27 +335,40 @@ class BuiltPlugin {
334
335
  };
335
336
  }
336
337
  async onLoad(context) {
337
- this.context = context;
338
+ // Use user-provided pluginRoot (from __dirname) if available, otherwise fall back to context.pluginRoot
339
+ // The user-provided path comes from Node's __dirname which automatically resolves to the real directory
340
+ // (following symlinks), making it work correctly in both local development and npm installed scenarios
341
+ const effectivePluginRoot = this.userProvidedPluginRoot || context.pluginRoot;
342
+ this.context = {
343
+ ...context,
344
+ pluginRoot: effectivePluginRoot
345
+ };
338
346
  context.logger.info('═══════════════════════════════════════════════════════');
339
347
  context.logger.info(`🚀 Loading Plugin: ${this.name} (${this.id}) v${this.version}`);
340
- context.logger.info(`📍 Plugin Root: ${context.pluginRoot}`);
348
+ context.logger.info(`📍 Plugin Root: ${effectivePluginRoot}`);
349
+ if (this.userProvidedPluginRoot) {
350
+ context.logger.info(` (user-provided via .pluginRoot(__dirname))`);
351
+ }
352
+ else {
353
+ context.logger.info(` (from MAJK context - consider using .pluginRoot(__dirname) for better reliability)`);
354
+ }
341
355
  context.logger.info(`🌐 HTTP Port: ${context.http.port}`);
342
356
  context.logger.info(`🔗 Base URL: ${context.http.baseUrl}`);
343
357
  context.logger.info('═══════════════════════════════════════════════════════');
344
358
  // Validate file paths now that we have pluginRoot context
345
359
  if (this.reactScreens.length > 0 && this.uiConfig) {
346
- const indexPath = path_1.default.join(context.pluginRoot, this.uiConfig.appDir || '', 'index.html');
360
+ const indexPath = path_1.default.join(this.context.pluginRoot, this.uiConfig.appDir || '', 'index.html');
347
361
  if (!fs_1.default.existsSync(indexPath)) {
348
- throw new PluginRuntimeError(`React app not built: ${indexPath} does not exist. Run "npm run build" in your UI directory to build the React app.`, 'onLoad', { appDir: this.uiConfig.appDir, indexPath, pluginRoot: context.pluginRoot });
362
+ throw new PluginRuntimeError(`React app not built: ${indexPath} does not exist. Run "npm run build" in your UI directory to build the React app.`, 'onLoad', { appDir: this.uiConfig.appDir, indexPath, pluginRoot: this.context.pluginRoot });
349
363
  }
350
364
  context.logger.info(`✅ React UI found at: ${indexPath}`);
351
365
  }
352
366
  // Validate HTML screen files
353
367
  for (const screen of this.htmlScreens) {
354
368
  if ('htmlFile' in screen) {
355
- const filePath = path_1.default.join(context.pluginRoot, screen.htmlFile);
369
+ const filePath = path_1.default.join(this.context.pluginRoot, screen.htmlFile);
356
370
  if (!fs_1.default.existsSync(filePath)) {
357
- throw new PluginRuntimeError(`HTML screen file not found: ${screen.htmlFile}. Create the HTML file at ${filePath} or fix the path.`, 'onLoad', { screen: screen.id, file: screen.htmlFile, resolvedPath: filePath, pluginRoot: context.pluginRoot });
371
+ throw new PluginRuntimeError(`HTML screen file not found: ${screen.htmlFile}. Create the HTML file at ${filePath} or fix the path.`, 'onLoad', { screen: screen.id, file: screen.htmlFile, resolvedPath: filePath, pluginRoot: this.context.pluginRoot });
358
372
  }
359
373
  context.logger.info(`✅ HTML screen file found: ${screen.htmlFile}`);
360
374
  }
@@ -434,11 +448,18 @@ class BuiltPlugin {
434
448
  async startServer() {
435
449
  const { port } = this.context.http;
436
450
  this.server = http_1.default.createServer(async (req, res) => {
451
+ const startTime = Date.now();
452
+ const method = req.method || 'UNKNOWN';
453
+ const url = req.url || '/';
437
454
  try {
455
+ // Log incoming request
456
+ this.context.logger.debug(`→ ${method} ${url}`);
438
457
  // CORS preflight
439
458
  if (req.method === 'OPTIONS') {
440
459
  res.writeHead(204, corsHeaders());
441
460
  res.end();
461
+ const duration = Date.now() - startTime;
462
+ this.context.logger.debug(`← ${method} ${url} 204 (${duration}ms)`);
442
463
  return;
443
464
  }
444
465
  // Health check
@@ -446,6 +467,8 @@ class BuiltPlugin {
446
467
  const health = await this.isHealthy();
447
468
  res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
448
469
  res.end(JSON.stringify({ ...health, plugin: this.name, version: this.version }));
470
+ const duration = Date.now() - startTime;
471
+ this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
449
472
  return;
450
473
  }
451
474
  // HTML screens virtual route
@@ -455,6 +478,8 @@ class BuiltPlugin {
455
478
  if (!screen) {
456
479
  res.writeHead(404, corsHeaders({ 'Content-Type': 'text/plain' }));
457
480
  res.end('HTML screen not found');
481
+ const duration = Date.now() - startTime;
482
+ this.context.logger.debug(`← ${method} ${url} 404 (${duration}ms)`);
458
483
  return;
459
484
  }
460
485
  res.writeHead(200, corsHeaders({ 'Content-Type': 'text/html; charset=utf-8' }));
@@ -470,6 +495,8 @@ class BuiltPlugin {
470
495
  res.writeHead(403, corsHeaders({ 'Content-Type': 'text/plain' }));
471
496
  res.end('Forbidden');
472
497
  this.context.logger.warn(`Path traversal attempt: ${screen.htmlFile}`);
498
+ const duration = Date.now() - startTime;
499
+ this.context.logger.debug(`← ${method} ${url} 403 (${duration}ms)`);
473
500
  return;
474
501
  }
475
502
  try {
@@ -479,13 +506,20 @@ class BuiltPlugin {
479
506
  this.context.logger.error(`Failed to read HTML file: ${error.message}`);
480
507
  res.writeHead(500, corsHeaders({ 'Content-Type': 'text/html' }));
481
508
  res.end('<!doctype html><html><body><h1>Error loading screen</h1></body></html>');
509
+ const duration = Date.now() - startTime;
510
+ this.context.logger.debug(`← ${method} ${url} 500 (${duration}ms)`);
511
+ return;
482
512
  }
483
513
  }
514
+ const duration = Date.now() - startTime;
515
+ this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
484
516
  return;
485
517
  }
486
518
  // React SPA
487
519
  if (req.method === 'GET' && this.uiConfig && req.url?.startsWith(this.uiConfig.base)) {
488
520
  serveSpa(this.uiConfig, req, res, this.context);
521
+ const duration = Date.now() - startTime;
522
+ this.context.logger.debug(`← ${method} ${url} ${res.statusCode || 200} (${duration}ms)`);
489
523
  return;
490
524
  }
491
525
  // API routes
@@ -502,6 +536,8 @@ class BuiltPlugin {
502
536
  method,
503
537
  hint: `Available routes: ${this.router.map(r => `${r.method} ${r.name}`).join(', ')}`
504
538
  }));
539
+ const duration = Date.now() - startTime;
540
+ this.context.logger.debug(`← ${method} ${pathname} 404 (${duration}ms)`);
505
541
  return;
506
542
  }
507
543
  const body = await readBody(req);
@@ -527,16 +563,16 @@ class BuiltPlugin {
527
563
  http: this.context.http
528
564
  };
529
565
  try {
530
- this.context.logger.debug(`📥 ${method} ${pathname}`, { params, body });
531
566
  const result = await route.handler(request, response, routeContext);
532
567
  if (!res.headersSent) {
533
568
  res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
534
569
  res.end(JSON.stringify(result ?? { success: true }));
535
570
  }
536
- this.context.logger.debug(`📤 ${method} ${pathname} - Success`);
571
+ const duration = Date.now() - startTime;
572
+ this.context.logger.debug(`← ${method} ${pathname} ${res.statusCode || 200} (${duration}ms)`);
537
573
  }
538
574
  catch (error) {
539
- this.context.logger.error(`❌ ${method} ${pathname} - Error: ${error.message}`);
575
+ this.context.logger.error(`API route error: ${method} ${pathname} - ${error.message}`);
540
576
  if (error.stack) {
541
577
  this.context.logger.error(error.stack);
542
578
  }
@@ -548,6 +584,8 @@ class BuiltPlugin {
548
584
  path: pathname
549
585
  }));
550
586
  }
587
+ const duration = Date.now() - startTime;
588
+ this.context.logger.debug(`← ${method} ${pathname} 500 (${duration}ms)`);
551
589
  }
552
590
  return;
553
591
  }
@@ -601,6 +639,7 @@ function definePlugin(id, name, version) {
601
639
  let _settings = null;
602
640
  let _onReady = null;
603
641
  let _healthCheck = null;
642
+ let _pluginRoot = undefined;
604
643
  const builder = {
605
644
  topbar(route, opts) {
606
645
  _topbars.push({
@@ -718,6 +757,13 @@ function definePlugin(id, name, version) {
718
757
  _healthCheck = fn;
719
758
  return this;
720
759
  },
760
+ pluginRoot(dir) {
761
+ if (!dir || typeof dir !== 'string') {
762
+ throw new PluginBuildError('pluginRoot must be a non-empty string', 'Pass __dirname from your plugin entry file: .pluginRoot(__dirname)', { provided: dir });
763
+ }
764
+ _pluginRoot = dir;
765
+ return this;
766
+ },
721
767
  build() {
722
768
  // ========== Build-Time Validation ==========
723
769
  // Validate React UI setup
@@ -811,7 +857,7 @@ function definePlugin(id, name, version) {
811
857
  // Return a class that instantiates BuiltPlugin
812
858
  return class extends BuiltPlugin {
813
859
  constructor() {
814
- super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _onReady, _healthCheck);
860
+ super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _onReady, _healthCheck, _pluginRoot);
815
861
  }
816
862
  };
817
863
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majkapp/plugin-kit",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Fluent builder framework for creating robust MAJK plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",