wirejs-scripts 3.0.103 → 3.0.105-payments

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.
Files changed (2) hide show
  1. package/bin.js +277 -41
  2. package/package.json +5 -3
package/bin.js CHANGED
@@ -3,8 +3,10 @@
3
3
  import process from 'process';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import http from 'http';
6
- import fs, { watch } from 'fs';
6
+ import fs from 'fs';
7
+ import os from 'os';
7
8
  import path from 'path';
9
+ import { fileURLToPath } from 'url';
8
10
 
9
11
  import webpack from 'webpack';
10
12
  import webpackConfigure from './configs/webpack.config.js';
@@ -14,14 +16,29 @@ import { contentType } from 'mime-types';
14
16
 
15
17
  import { JSDOM } from 'jsdom';
16
18
  import { useJSDOM } from 'wirejs-dom/v2';
17
- import { requiresContext, Context, CookieJar } from 'wirejs-resources';
19
+ import {
20
+ requiresContext,
21
+ Context,
22
+ CookieJar,
23
+ Endpoint,
24
+ SystemAttribute,
25
+ } from 'wirejs-resources';
18
26
  import { prebuildApi } from 'wirejs-resources/internal';
19
27
 
28
+ import * as DefaultGateway from 'default-gateway';
29
+ import * as NatPmp from 'nat-pmp';
30
+
20
31
  const CWD = process.cwd();
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
21
34
  const getWebpackConfig = () => webpackConfigure(process.env, process.argv);
22
35
  const [_nodeBinPath, _scriptPath, action, ...actionArgs] = process.argv;
23
36
  const [subAction] = actionArgs;
24
37
  const processes = [];
38
+ let publicIp = null;
39
+ let networkIp = null;
40
+ let httpPort = 3000;
41
+ let wsPort = 3001;
25
42
 
26
43
  const logger = {
27
44
  log(...items) {
@@ -44,24 +61,169 @@ globalThis.fetch = (url, ...args) => {
44
61
  try {
45
62
  return fetch(new URL(url), ...args);
46
63
  } catch {
47
- return fetch(`http://localhost:3000${url}`, ...args);
64
+ return fetch(`http://localhost:${httpPort}${url}`, ...args);
48
65
  }
49
66
  } else {
50
67
  return oldFetch(url, ...args);
51
68
  }
52
69
  }
53
70
 
71
+ function isWSL() {
72
+ if (os.platform() !== 'linux') return false;
73
+ if (process.env.WSL_DISTRO_NAME) return true;
74
+ try {
75
+ const version = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
76
+ return version.includes('microsoft');
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function getLocalIPv4() {
83
+ if (isWSL()) {
84
+ try {
85
+ const $ = "\\$";
86
+ const command = `powershell.exe -Command "& { (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { ${$}_.InterfaceAlias -notlike '*WSL*' -and ${$}_.IPAddress -notlike '169.*' } | Select-Object -First 1 -ExpandProperty IPAddress) }"`;
87
+ const output = execSync(command, { cwd: CWD, encoding: 'utf8' }).toString().trim();
88
+ return output || null;
89
+ } catch (err) {
90
+ console.error("❌ Failed to get Windows IPv4 from WSL:", err);
91
+ return null;
92
+ }
93
+ } else {
94
+ const interfaces = os.networkInterfaces();
95
+ for (const name of Object.keys(interfaces)) {
96
+ for (const iface of interfaces[name]) {
97
+ if (iface.family === 'IPv4' && !iface.internal) {
98
+ return iface.address;
99
+ }
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * For Windows Subsystem for Linux (WSL) environments, get the IP address
108
+ * Windows has assigned to the Linux container.
109
+ *
110
+ * @returns
111
+ */
112
+ function getWSLIP() {
113
+ const output = execSync('hostname -I').toString().trim();
114
+ return output.split(' ')[0];
115
+ }
116
+
117
+ async function getGateway() {
118
+ if (isWSL()) {
119
+ const probeCommand = [
120
+ 'powershell.exe -Command',
121
+ `"Get-NetRoute -DestinationPrefix '0.0.0.0/0'`,
122
+ `| Sort-Object RouteMetric`,
123
+ `| Select-Object -First 1 -ExpandProperty NextHop"`,
124
+ ].join(' ');
125
+ return execSync(probeCommand).toString().replace(/\r/, '').trim();
126
+ } else {
127
+ return (await DefaultGateway.gateway4async()).gateway;
128
+ }
129
+ }
130
+
131
+ function portProxyRuleExists(listenPort, wslIp, wslPort) {
132
+ try {
133
+ const output = execSync(`netsh interface portproxy show all`, {
134
+ shell: 'powershell.exe'
135
+ }).toString();
136
+
137
+ const regex = new RegExp(
138
+ `0\\.0\\.0\\.0\\s+${listenPort}\\s+${wslIp.replace(/\./g, '\\.')}\\s+${wslPort}`
139
+ );
140
+
141
+ return regex.test(output);
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ function firewallRuleExists(port) {
148
+ try {
149
+ const command = `powershell.exe -Command "Get-NetFirewallRule -DisplayName 'WSLForward${port}'"`;
150
+ const result = execSync(command).toString().trim();
151
+ return result.length > 0;
152
+ } catch (err) {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ async function startForwardingWSLPort(wslPort = httpPort, publicPort = httpPort) {
158
+ if (!isWSL()) throw new Error("Not running in WSL.");
159
+
160
+ const wslIP = getWSLIP();
161
+ console.log(`▶ Spawning Windows-side forward: :${publicPort} → ${wslIP}:${wslPort}`);
162
+
163
+ if (!portProxyRuleExists(publicPort, wslIP, wslPort)) {
164
+ const forwardPortCommand = [
165
+ 'Start-Process',
166
+ 'netsh',
167
+ '-Verb RunAs',
168
+ '-ArgumentList',
169
+ `'interface portproxy add v4tov4 `,
170
+ `listenport=${publicPort} listenaddress=0.0.0.0`,
171
+ `connectport=${wslPort} connectaddress=${wslIP}'`
172
+ ].join(' ');
173
+ execSync(`powershell.exe -Command "${forwardPortCommand}"`);
174
+ }
175
+
176
+ if (!firewallRuleExists(publicPort)) {
177
+ const openFirewallCommand = [
178
+ 'Start-Process',
179
+ 'powershell',
180
+ '-Verb',
181
+ 'RunAs',
182
+ '-ArgumentList',
183
+ `'-Command "New-NetFirewallRule -DisplayName WSLForward${publicPort} -Direction Inbound -LocalPort ${publicPort} -Protocol TCP -Action Allow"'`
184
+ ].join(' ');
185
+ execSync(`powershell.exe -Command "${openFirewallCommand}"`);
186
+ }
187
+ }
188
+
54
189
  /**
55
190
  *
56
191
  * @param {http.IncomingMessage} req
57
192
  * @returns
58
193
  */
59
- function createContext(req) {
60
- const { url, headers } = req;
194
+ async function createContext(req) {
195
+ const { url, headers, method } = req;
196
+ const body = method.toLowerCase() === 'post'
197
+ ? (await postData(req))
198
+ : undefined;
61
199
  const origin = headers.origin || `http://${headers.host}`;
62
200
  const location = new URL(`${origin}${url}`);
63
201
  const cookies = new CookieJar(headers.cookie);
64
- return new Context({ cookies, location });
202
+ return new Context({
203
+ cookies,
204
+ location,
205
+ httpMethod: method,
206
+ requestHeaders: headers,
207
+ requestBody: body,
208
+ runtimeAttributes: [
209
+ new SystemAttribute('wirejs', 'deployment-type', {
210
+ description: 'Deployment under which your system is running.',
211
+ value: 'dev'
212
+ }),
213
+ new SystemAttribute('wirejs', 'http-origin-local', {
214
+ description: 'HTTP origin (base address) to use for local development.',
215
+ value: `http://localhost:${httpPort}`,
216
+ }),
217
+ new SystemAttribute('wirejs', 'http-origin-network', {
218
+ description: 'HTTP origin (base address) for machines on your network to use.',
219
+ value: `http://${networkIp}:${httpPort}`,
220
+ }),
221
+ new SystemAttribute('wirejs', 'http-origin-public', {
222
+ description: 'HTTP origin (base address) for machines outside your network to use. Only populated for `npm run start:public`, and only accessible in environments that support NAT-PMP.',
223
+ value: publicIp ? `http://${publicIp}:${httpPort}` : null,
224
+ }),
225
+ ]
226
+ });
65
227
  }
66
228
 
67
229
  /**
@@ -101,20 +263,15 @@ async function callApiMethod(api, call, context) {
101
263
 
102
264
  /**
103
265
  *
104
- * @param {http.IncomingMessage} req
266
+ * @param {Context} context
105
267
  * @param {http.ServerResponse} res
106
268
  * @returns
107
269
  */
108
- async function handleApiResponse(req, res) {
109
- const {
110
- headers, url, method, params, query,
111
- baseUrl, originalUrl, trailers
112
- } = req;
113
-
114
- const context = createContext(req);
270
+ async function handleApiResponse(context, res) {
271
+ const url = context.location.pathname;
115
272
 
116
273
  if (url === '/api') {
117
- const body = await postData(req);
274
+ const body = context.requestBody;
118
275
  const calls = JSON.parse(body);
119
276
  logger.info('handling API request', body);
120
277
 
@@ -141,7 +298,9 @@ async function handleApiResponse(req, res) {
141
298
  );
142
299
  }
143
300
 
144
- res.setHeader('Content-Type', 'application/json; charset=utf-8')
301
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
302
+ // internal API isn't allowed to set individual response headers, codes, etc.
303
+ // because they may be batched.
145
304
  res.end(JSON.stringify(
146
305
  responses
147
306
  ));
@@ -155,23 +314,56 @@ async function handleApiResponse(req, res) {
155
314
 
156
315
  /**
157
316
  *
158
- * @param {http.IncomingMessage} req
317
+ * @param {Context} context
318
+ * @param {http.ServerResponse} res
319
+ * @returns
320
+ */
321
+ async function tryEndpointPath(context, res) {
322
+ /**
323
+ * @type Endpoint
324
+ */
325
+ let matchingEndpoint = undefined;
326
+
327
+ try {
328
+ const apiPath = path.join(CWD, 'api', 'dist', 'index.js');
329
+ await import(`${apiPath}?cache-id=${new Date().getTime()}`);
330
+ const allHandlers = [...Endpoint.list()];
331
+ const matchingHandlers = allHandlers
332
+ .filter(e => globMatch(e.path, context.location.pathname));
333
+ matchingEndpoint = matchingHandlers.sort(byLength).pop();
334
+ } catch (error) {
335
+ return;
336
+ }
337
+
338
+ if (!matchingEndpoint) return;
339
+
340
+ const response = await matchingEndpoint.handle(context)
341
+ setResponseDetailsFromContext(context, res);
342
+ res.end(response);
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ *
348
+ * @param {Context} context
159
349
  * @returns
160
350
  */
161
- function fullPathFrom(req) {
162
- const relativePath = req.url === '/' ? 'index.html' : req.url;
351
+ function fullPathFrom(context) {
352
+ const relativePath = context.location.pathname === '/'
353
+ ? 'index.html'
354
+ : context.location.pathname;
163
355
  return path.join(CWD, 'dist', relativePath);
164
356
  }
165
357
 
166
358
 
167
359
  /**
168
360
  *
169
- * @param {http.IncomingMessage} req
361
+ * @param {Context} context
170
362
  * @param {http.ServerResponse} res
171
363
  * @returns
172
364
  */
173
- async function tryStaticPath(req, res) {
174
- const fullPath = fullPathFrom(req);
365
+ async function tryStaticPath(context, res) {
366
+ const fullPath = fullPathFrom(context);
175
367
 
176
368
  logger.info('checking static', fullPath);
177
369
  if (!fs.existsSync(fullPath)) return false;
@@ -250,12 +442,11 @@ function routeSSR(context, forceExt) {
250
442
  }
251
443
 
252
444
  /**
253
- * @param {http.IncomingMessage} req
445
+ * @param {Context} context
254
446
  * @param {http.ServerResponse} res
255
447
  * @returns
256
448
  */
257
- async function trySSRScriptPath(req, res) {
258
- const context = createContext(req);
449
+ async function trySSRScriptPath(context, res) {
259
450
  const srcPath = routeSSR(context);
260
451
  if (!srcPath) return false;
261
452
 
@@ -275,13 +466,26 @@ async function trySSRScriptPath(req, res) {
275
466
 
276
467
  /**
277
468
  *
278
- * @param {http.IncomingMessage} req
469
+ * @param {Context} context
470
+ * @param {http.ServerResponse} res
471
+ */
472
+ function setResponseDetailsFromContext(context, res) {
473
+ for (const [k, v] of Object.entries(context.responseHeaders)) {
474
+ res.setHeader(k, v);
475
+ }
476
+ if (context.locationIsDirty) {
477
+ res.setHeader('Location', context.location.href);
478
+ }
479
+ if (context.responseCode) res.statusCode = context.responseCode;
480
+ }
481
+
482
+ /**
483
+ *
484
+ * @param {Context} context
279
485
  * @param {http.ServerResponse} res
280
486
  * @returns
281
487
  */
282
- async function trySSRPath(req, res) {
283
- const context = createContext(req);
284
-
488
+ async function trySSRPath(context, res) {
285
489
  const asJSPath = context.location.pathname.replace(/\.(\w+)$/, '') + '.js';
286
490
  const srcPath = routeSSR(context, 'js');
287
491
  if (!srcPath) return false;
@@ -300,6 +504,7 @@ async function trySSRPath(req, res) {
300
504
  res.setHeader('Content-Type', contentType(
301
505
  context.location.pathname.split('.').pop()
302
506
  ));
507
+ setResponseDetailsFromContext(context, res);
303
508
  res.end(doc);
304
509
  } else {
305
510
  const doctype = doc.parentNode.doctype?.name || '';
@@ -316,7 +521,8 @@ async function trySSRPath(req, res) {
316
521
  doc.parentNode.body.appendChild(script);
317
522
  }
318
523
 
319
- res.setHeader('Content-type', 'text/html; charset=utf-8');
524
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
525
+ setResponseDetailsFromContext(context, res);
320
526
  res.end([
321
527
  doctype ? `<!doctype ${doctype}>\n` : '',
322
528
  doc.outerHTML
@@ -345,16 +551,16 @@ async function trySSRPath(req, res) {
345
551
  */
346
552
  async function handleRequest(req, res, compiler) {
347
553
  logger.info('received', JSON.stringify({ url: req.url }, null, 2));
554
+ const context = await createContext(req);
348
555
 
349
556
  if (req.url.startsWith('/api')) {
350
- return handleApiResponse(req, res, compiler);
557
+ return handleApiResponse(context, res, compiler);
351
558
  }
352
559
 
353
- // const fs = compiler.outputFileSystem;
354
-
355
- if (await tryStaticPath(req, res, fs)) return;
356
- if (await trySSRScriptPath(req, res, fs)) return;
357
- if (await trySSRPath(req, res, fs)) return;
560
+ if (await tryStaticPath(context, res, fs)) return;
561
+ if (await trySSRScriptPath(context, res, fs)) return;
562
+ if (await trySSRPath(context, res, fs)) return;
563
+ if (await tryEndpointPath(context, res, fs)) return;
358
564
 
359
565
  // if we've made it this far, we don't have what you're looking for
360
566
  res.statusCode = '404';
@@ -391,7 +597,7 @@ async function compile(watch = false) {
391
597
  outdir: path.join(apiDir, 'dist'),
392
598
  platform: 'node',
393
599
  bundle: true,
394
- external: ['./node_modules/*'],
600
+ packages: 'external',
395
601
  format: 'esm',
396
602
  plugins: [{
397
603
  name: 'post-build-rebuild-api-client',
@@ -413,21 +619,51 @@ async function compile(watch = false) {
413
619
  await prebuild.rebuild();
414
620
  prebuild.watch();
415
621
 
622
+ if (actionArgs.includes('--public') || actionArgs.includes('-p')) {
623
+ console.log("Attempting to open external port.");
624
+ const gatewayIp = await getGateway();
625
+ const natPmpClient = NatPmp.connect(gatewayIp);
626
+ natPmpClient.externalIp((err, info) => {
627
+ if (err) throw err;
628
+ const ip = info.ip.join('.');
629
+ console.log(`Found external address: ${ip} ... trying to connect.`);
630
+ for (const port of [httpPort, wsPort]) {
631
+ natPmpClient.portMapping({
632
+ private: port,
633
+ public: port,
634
+ ttl: 3600,
635
+ }, (err, info) => {
636
+ if (err) throw err;
637
+ console.log(`Listening externally at ${ip}:${port}`);
638
+ publicIp = ip
639
+ });
640
+ };
641
+ });
642
+ if (isWSL()) startForwardingWSLPort();
643
+ }
644
+
416
645
  webpack({
417
646
  ...getWebpackConfig(),
418
647
  mode: 'development',
419
648
  watch: true
420
649
  }, () => {
421
650
  console.log();
422
- console.log('Compiled: http://localhost:3000/');
651
+ console.log('Compiled.\n');
652
+ console.log(`Local\nhttp://localhost:${httpPort}/\n`);
653
+ networkIp = getLocalIPv4();
654
+ if (networkIp) {
655
+ console.log(`Network\nhttp://${networkIp}:${httpPort}/\n`)
656
+ }
657
+ if (publicIp) {
658
+ console.log(`Public\nhttp://${publicIp}:${httpPort}/\n`);
659
+ }
423
660
  }).run(() => { });
424
661
 
425
662
  logger.log('Starting server...');
426
663
  const server = http.createServer(handleRequest);
427
- server.listen(3000).on('listening', () => {
428
- console.log('Started listening on http://localhost:3000/')
664
+ server.listen(httpPort).on('listening', () => {
665
+ console.log(`Started listening on port ${httpPort}`);
429
666
  });
430
-
431
667
  } else {
432
668
  logger.log('prebundling JS');
433
669
  await esbuild.build({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-scripts",
3
- "version": "3.0.103",
3
+ "version": "3.0.105-payments",
4
4
  "description": "Basic build and start commands for wirejs apps",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,17 +24,19 @@
24
24
  "dependencies": {
25
25
  "copy-webpack-plugin": "^10.2.4",
26
26
  "css-loader": "^5.2.0",
27
+ "default-gateway": "^7.2.2",
27
28
  "esbuild": "^0.24.2",
28
29
  "file-loader": "^6.2.0",
29
30
  "glob": "^7.2.0",
30
31
  "jsdom": "^25.0.1",
31
32
  "marked": "^2.0.1",
32
33
  "mime-types": "^2.1.35",
34
+ "nat-pmp": "^1.0.0",
33
35
  "raw-loader": "^4.0.2",
34
36
  "rimraf": "^6.0.1",
35
37
  "style-loader": "^2.0.0",
36
38
  "webpack": "^5.97.1",
37
- "wirejs-dom": "^1.0.41",
38
- "wirejs-resources": "^0.1.105"
39
+ "wirejs-dom": "^1.0.42",
40
+ "wirejs-resources": "^0.1.107-payments"
39
41
  }
40
42
  }