quackage 1.2.1 → 1.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quackage",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Building. Testing. Quacking. Reloading.",
5
5
  "main": "source/Quackage-CLIProgram.js",
6
6
  "scripts": {
@@ -65,7 +65,7 @@
65
65
  "mocha": "10.4.0",
66
66
  "npm-check-updates": "^18.0.1",
67
67
  "nyc": "^15.1.0",
68
- "pict-provider-theme": "^0.0.1",
68
+ "pict-provider-theme": "^0.0.4",
69
69
  "pict-service-commandlineutility": "^1.0.19",
70
70
  "retold-harness": "^1.1.10",
71
71
  "vinyl-buffer": "^1.0.1",
@@ -65,8 +65,9 @@ let _Pict = new libCLIProgram(
65
65
  require('./commands/html_example_serving/Quackage-Command-ExamplesServe.js'),
66
66
  require('./commands/html_example_serving/Quackage-Command-Examples.js'),
67
67
 
68
- // Theme bundle compilation (pict-provider-theme)
69
- require('pict-provider-theme/source/cli/Quackage-Command-ThemeBuild.js')
68
+ // Theme bundle compilation + screenshot tooling (pict-provider-theme)
69
+ require('pict-provider-theme/source/cli/Quackage-Command-ThemeBuild.js'),
70
+ require('pict-provider-theme/source/cli/Quackage-Command-ThemeScreenshot.js')
70
71
  ]);
71
72
 
72
73
  // Instantiate the file persistence service
@@ -2,6 +2,8 @@ const libPict = require('pict');
2
2
  const libFS = require('fs');
3
3
  const libPath = require('path');
4
4
  const libHTTP = require('http');
5
+ const libHTTPS = require('https');
6
+ const libURL = require('url');
5
7
 
6
8
  const libFable = require('fable');
7
9
  const libBookstoreSchema = require('retold-harness/source/schemas/Retold-Harness-Service-Schema-Bookstore.js');
@@ -533,6 +535,235 @@ ${tmpExampleListItems} </ul>
533
535
  });
534
536
  }
535
537
 
538
+ // --- Serve static + reverse-proxy a configured upstream ---
539
+
540
+ /**
541
+ * Serve example files out of pExamplesFolder, but reverse-proxy any request
542
+ * whose path starts with one of pProxyConfig.paths to pProxyConfig.upstream.
543
+ *
544
+ * Used by mode='proxy' so cookie-credentialed APIs (Headlight, etc.) appear
545
+ * same-origin to the browser and CORS doesn't get in the way.
546
+ */
547
+ serveStaticWithProxy(pExamplesFolder, pIndexHTML, pPort, pExamples, pProjectName, pProxyConfig, fCallback)
548
+ {
549
+ const tmpUpstreamParsed = libURL.parse(pProxyConfig.upstream);
550
+ const tmpUpstreamClient = (tmpUpstreamParsed.protocol === 'https:') ? libHTTPS : libHTTP;
551
+ const tmpUpstreamHost = tmpUpstreamParsed.host;
552
+ const tmpUpstreamOrigin = `${tmpUpstreamParsed.protocol}//${tmpUpstreamHost}`;
553
+
554
+ const tmpShouldProxy = (pRequestPath) =>
555
+ {
556
+ for (let i = 0; i < pProxyConfig.paths.length; i++)
557
+ {
558
+ if (pRequestPath === pProxyConfig.paths[i] || pRequestPath.indexOf(pProxyConfig.paths[i]) === 0)
559
+ {
560
+ return true;
561
+ }
562
+ }
563
+ return false;
564
+ };
565
+
566
+ const tmpRewriteSetCookie = (pCookies) =>
567
+ {
568
+ if (!pCookies) { return pCookies; }
569
+ const tmpArray = Array.isArray(pCookies) ? pCookies : [pCookies];
570
+ return tmpArray.map((pCookie) => String(pCookie)
571
+ .replace(/;\s*Domain=[^;]+/i, '')
572
+ .replace(/;\s*Secure/i, ''));
573
+ };
574
+
575
+ const tmpProxyRequest = (pRequest, pResponse) =>
576
+ {
577
+ const tmpProxyHeaders = Object.assign({}, pRequest.headers);
578
+ if (pProxyConfig.rewriteOrigin)
579
+ {
580
+ tmpProxyHeaders.host = tmpUpstreamHost;
581
+ if (tmpProxyHeaders.origin) { tmpProxyHeaders.origin = tmpUpstreamOrigin; }
582
+ if (tmpProxyHeaders.referer) { tmpProxyHeaders.referer = tmpUpstreamOrigin + '/'; }
583
+ }
584
+ delete tmpProxyHeaders['accept-encoding'];
585
+
586
+ const tmpProxyOptions =
587
+ {
588
+ protocol: tmpUpstreamParsed.protocol,
589
+ hostname: tmpUpstreamParsed.hostname,
590
+ port: tmpUpstreamParsed.port || (tmpUpstreamParsed.protocol === 'https:' ? 443 : 80),
591
+ method: pRequest.method,
592
+ path: pRequest.url,
593
+ headers: tmpProxyHeaders
594
+ };
595
+
596
+ const tmpProxyReq = tmpUpstreamClient.request(tmpProxyOptions, (pProxyRes) =>
597
+ {
598
+ const tmpResponseHeaders = Object.assign({}, pProxyRes.headers);
599
+ if (pProxyConfig.rewriteCookies && tmpResponseHeaders['set-cookie'])
600
+ {
601
+ tmpResponseHeaders['set-cookie'] = tmpRewriteSetCookie(tmpResponseHeaders['set-cookie']);
602
+ }
603
+ pResponse.writeHead(pProxyRes.statusCode || 502, tmpResponseHeaders);
604
+ pProxyRes.pipe(pResponse);
605
+ });
606
+
607
+ tmpProxyReq.on('error', (pError) =>
608
+ {
609
+ this.log.error(`Proxy upstream error for ${pRequest.method} ${pRequest.url}: ${pError.message}`);
610
+ if (!pResponse.headersSent)
611
+ {
612
+ pResponse.writeHead(502, { 'Content-Type': 'text/plain' });
613
+ }
614
+ pResponse.end(`Bad gateway: ${pError.message}`);
615
+ });
616
+
617
+ pRequest.pipe(tmpProxyReq);
618
+ };
619
+
620
+ const tmpServeStaticFile = (pRequest, pResponse) =>
621
+ {
622
+ let tmpRequestURL = pRequest.url.split('?')[0];
623
+
624
+ if (tmpRequestURL === '/' || tmpRequestURL === '/index.html')
625
+ {
626
+ pResponse.writeHead(200, { 'Content-Type': 'text/html' });
627
+ pResponse.end(pIndexHTML);
628
+ return;
629
+ }
630
+
631
+ let tmpFilePath = libPath.join(pExamplesFolder, decodeURIComponent(tmpRequestURL));
632
+ if (!tmpFilePath.startsWith(pExamplesFolder))
633
+ {
634
+ pResponse.writeHead(403);
635
+ pResponse.end('Forbidden');
636
+ return;
637
+ }
638
+ if (libFS.existsSync(tmpFilePath) && libFS.statSync(tmpFilePath).isDirectory())
639
+ {
640
+ tmpFilePath = libPath.join(tmpFilePath, 'index.html');
641
+ }
642
+ if (!libFS.existsSync(tmpFilePath))
643
+ {
644
+ pResponse.writeHead(404);
645
+ pResponse.end('Not Found');
646
+ return;
647
+ }
648
+
649
+ let tmpExtension = libPath.extname(tmpFilePath).toLowerCase();
650
+ let tmpMimeType = this.getMimeType(tmpExtension);
651
+ try
652
+ {
653
+ let tmpContent = libFS.readFileSync(tmpFilePath);
654
+ pResponse.writeHead(200, { 'Content-Type': tmpMimeType });
655
+ pResponse.end(tmpContent);
656
+ }
657
+ catch (pReadError)
658
+ {
659
+ pResponse.writeHead(500);
660
+ pResponse.end('Internal Server Error');
661
+ }
662
+ };
663
+
664
+ const tmpServer = libHTTP.createServer((pRequest, pResponse) =>
665
+ {
666
+ const tmpPath = pRequest.url.split('?')[0];
667
+ if (tmpShouldProxy(tmpPath))
668
+ {
669
+ tmpProxyRequest(pRequest, pResponse);
670
+ }
671
+ else
672
+ {
673
+ tmpServeStaticFile(pRequest, pResponse);
674
+ }
675
+ });
676
+
677
+ tmpServer.listen(pPort, () =>
678
+ {
679
+ this.log.info(`##############################################`);
680
+ this.log.info(` Example server running at http://localhost:${pPort}/`);
681
+ this.log.info(` Project: ${pProjectName}`);
682
+ this.log.info(` Proxy upstream: ${pProxyConfig.upstream}`);
683
+ this.log.info(` Proxied paths: ${pProxyConfig.paths.join(', ')}`);
684
+ this.log.info(` Serving ${pExamples.length} example(s):`);
685
+ for (let i = 0; i < pExamples.length; i++)
686
+ {
687
+ this.log.info(` - ${pExamples[i].DisplayName}: http://localhost:${pPort}/${pExamples[i].RelativePath}`);
688
+ }
689
+ this.log.info(`##############################################`);
690
+ this.log.info(`Press Ctrl+C to stop.`);
691
+ });
692
+
693
+ tmpServer.on('error', (pError) =>
694
+ {
695
+ if (pError.code === 'EADDRINUSE')
696
+ {
697
+ this.log.error(`Port ${pPort} is already in use. Try specifying a different port with -p.`);
698
+ }
699
+ else
700
+ {
701
+ this.log.error(`Server error: ${pError.message}`);
702
+ }
703
+ return fCallback(pError);
704
+ });
705
+ }
706
+
707
+ // --- Per-project examples configuration ---
708
+
709
+ /**
710
+ * Resolve the host project's examples configuration from its package.json.
711
+ * Reads `quackageExamples` (preferred) or `quack.examples` (alias).
712
+ *
713
+ * Recognised shapes:
714
+ *
715
+ * "quackageExamples": { "mode": "bookstore" } // default — Meadow + SQLite + bookstore data
716
+ * "quackageExamples": { "mode": "static" } // serve files only, no API
717
+ * "quackageExamples": {
718
+ * "mode": "proxy",
719
+ * "proxy": {
720
+ * "upstream": "https://fieldbook.qa.headlight.com",
721
+ * "paths": ["/1.0/", "/CheckSession", "/Authenticate", "/Report/"],
722
+ * "rewriteCookies": true,
723
+ * "rewriteOrigin": true
724
+ * }
725
+ * }
726
+ *
727
+ * Defaults (mode='proxy'):
728
+ * paths = ["/1.0/"]
729
+ * rewriteCookies = true (strip Domain= and Secure so localhost keeps the session)
730
+ * rewriteOrigin = true (rewrite Host/Origin/Referer to the upstream)
731
+ */
732
+ resolveExamplesConfig()
733
+ {
734
+ const tmpPackage = (this.fable && this.fable.AppData && this.fable.AppData.Package) || {};
735
+ const tmpRaw = tmpPackage.quackageExamples
736
+ || (tmpPackage.quack && tmpPackage.quack.examples)
737
+ || {};
738
+
739
+ const tmpMode = (typeof tmpRaw.mode === 'string') ? tmpRaw.mode.toLowerCase() : 'bookstore';
740
+ const tmpResolved = { mode: tmpMode };
741
+
742
+ if (tmpMode === 'proxy')
743
+ {
744
+ const tmpProxy = tmpRaw.proxy || {};
745
+ tmpResolved.proxy =
746
+ {
747
+ upstream: (typeof tmpProxy.upstream === 'string' && tmpProxy.upstream) ? tmpProxy.upstream.replace(/\/+$/, '') : null,
748
+ paths: (Array.isArray(tmpProxy.paths) && tmpProxy.paths.length) ? tmpProxy.paths.slice() : ['/1.0/'],
749
+ rewriteCookies: (tmpProxy.rewriteCookies !== false),
750
+ rewriteOrigin: (tmpProxy.rewriteOrigin !== false)
751
+ };
752
+ // Allow env-var override for the upstream so devs can switch envs without
753
+ // editing package.json: `UPSTREAM=https://fieldbook.headlight.com npx quack examples`.
754
+ if (process.env.QUACKAGE_PROXY_UPSTREAM)
755
+ {
756
+ tmpResolved.proxy.upstream = process.env.QUACKAGE_PROXY_UPSTREAM.replace(/\/+$/, '');
757
+ }
758
+ else if (process.env.UPSTREAM)
759
+ {
760
+ tmpResolved.proxy.upstream = process.env.UPSTREAM.replace(/\/+$/, '');
761
+ }
762
+ }
763
+
764
+ return tmpResolved;
765
+ }
766
+
536
767
  // --- Serve examples ---
537
768
 
538
769
  serveExamples(pExamplesFolder, pPort, fCallback)
@@ -555,7 +786,28 @@ ${tmpExampleListItems} </ul>
555
786
 
556
787
  let tmpIndexHTML = this.generateIndexHTML(tmpProjectName, tmpExamples, tmpPort);
557
788
 
558
- // Initialize the bookstore API (retold-harness + SQLite)
789
+ const tmpConfig = this.resolveExamplesConfig();
790
+
791
+ if (tmpConfig.mode === 'proxy')
792
+ {
793
+ if (!tmpConfig.proxy.upstream)
794
+ {
795
+ this.log.error(`quackageExamples.mode is 'proxy' but no upstream URL was configured.`);
796
+ this.log.error(`Set quackageExamples.proxy.upstream in package.json (or QUACKAGE_PROXY_UPSTREAM / UPSTREAM env var).`);
797
+ return fCallback(new Error('Proxy mode requires an upstream.'));
798
+ }
799
+ this.log.info(`Examples mode: proxy → ${tmpConfig.proxy.upstream}`);
800
+ this.log.info(`Proxying paths: ${tmpConfig.proxy.paths.join(', ')}`);
801
+ return this.serveStaticWithProxy(tmpExamplesFolder, tmpIndexHTML, tmpPort, tmpExamples, tmpProjectName, tmpConfig.proxy, fCallback);
802
+ }
803
+
804
+ if (tmpConfig.mode === 'static')
805
+ {
806
+ this.log.info(`Examples mode: static (no API)`);
807
+ return this.serveStaticHTTP(tmpExamplesFolder, tmpIndexHTML, tmpPort, fCallback);
808
+ }
809
+
810
+ // Default: bookstore API
559
811
  this.log.info(`Initializing bookstore API with SQLite ...`);
560
812
  this.initializeBookstoreAPI(tmpPort, tmpExamplesFolder,
561
813
  (pError, pHarnessFable) =>