|
1 | | -/* eslint-disable import/no-extraneous-dependencies */ |
2 | | -const fs = require('fs'); |
3 | | -const path = require('path'); |
4 | | -const liveServer = require('live-server'); |
5 | | -const getNet = require('./getNet'); |
| 1 | +const fs = require('node:fs'); |
| 2 | +const path = require('node:path'); |
| 3 | +const http = require('node:http'); |
| 4 | +const ngrok = require('@ngrok/ngrok'); |
| 5 | +const os = require('node:os'); |
6 | 6 |
|
7 | | -const serverCrt = path.resolve(__dirname, 'server.crt'); |
8 | | -const serverKey = path.resolve(__dirname, 'server.key'); |
| 7 | +const PORT = 5500; |
| 8 | +const PROJECT_ROOT = path.resolve(__dirname, '..'); |
| 9 | +const NGROK_CONFIG_PATHS = [ |
| 10 | + path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'), |
| 11 | + path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'), |
| 12 | + path.join(os.homedir(), '.ngrok2', 'ngrok.yml'), |
| 13 | +]; |
9 | 14 |
|
10 | 15 | main(); |
11 | 16 |
|
12 | 17 | async function main() { |
13 | | - const { ip: host, port } = await getNet('dev'); |
14 | | - process.cwd = () => __dirname; |
15 | | - liveServer.start({ |
16 | | - open: false, |
17 | | - port, |
18 | | - host, |
19 | | - cors: true, |
20 | | - root: '../', |
21 | | - ignore: 'node_modules,platforms,plugins', |
22 | | - file: 'index.html', |
23 | | - https: { |
24 | | - cert: fs.readFileSync(serverCrt), |
25 | | - key: fs.readFileSync(serverKey), |
26 | | - passphrase: '1234', |
27 | | - }, |
28 | | - middleware: [(req, res, next) => { |
29 | | - const url = req.originalUrl; |
30 | | - const www = '../platforms/android/app/src/main/assets/www/'; |
31 | | - |
32 | | - if (url === '/cordova.js') { |
33 | | - const file = path.resolve(__dirname, www, 'cordova.js'); |
34 | | - sendFile(res, file); |
35 | | - return; |
36 | | - } |
| 18 | + try { |
| 19 | + // Get ngrok authtoken |
| 20 | + const authtoken = await getNgrokAuthtoken(); |
| 21 | + |
| 22 | + // Create HTTP server |
| 23 | + const server = http.createServer((req, res) => { |
| 24 | + handleRequest(req, res); |
| 25 | + }); |
| 26 | + |
| 27 | + // Start local server |
| 28 | + server.listen(PORT, () => { |
| 29 | + console.log(`\n✓ Local server running on port ${PORT}`); |
| 30 | + }); |
| 31 | + |
| 32 | + // Create ngrok tunnel |
| 33 | + console.log('✓ Starting ngrok tunnel...'); |
| 34 | + |
| 35 | + // Try to use a static domain if available (check ngrok config) |
| 36 | + const domain = getStaticDomain(); |
| 37 | + const connectOptions = { |
| 38 | + addr: PORT, |
| 39 | + authtoken: authtoken |
| 40 | + }; |
| 41 | + |
| 42 | + if (domain) { |
| 43 | + connectOptions.domain = domain; |
| 44 | + console.log(` Using static domain: ${domain}`); |
| 45 | + } |
| 46 | + |
| 47 | + const listener = await ngrok.connect(connectOptions); |
37 | 48 |
|
38 | | - if (url === '/cordova_plugins.js') { |
39 | | - const file = path.resolve(__dirname, www, 'cordova_plugins.js'); |
40 | | - sendFile(res, file); |
41 | | - return; |
| 49 | + const url = listener.url(); |
| 50 | + console.log('\n' + '='.repeat(60)); |
| 51 | + console.log('🚀 Plugin available at:'); |
| 52 | + console.log(` ${url}/dist.zip`); |
| 53 | + console.log('='.repeat(60) + '\n'); |
| 54 | + |
| 55 | + if (!domain) { |
| 56 | + console.log('💡 TIP: URL changes each time on free plan.'); |
| 57 | + console.log(' To get a permanent URL, claim your free static domain:'); |
| 58 | + console.log(' 1. Visit: https://dashboard.ngrok.com/domains'); |
| 59 | + console.log(' 2. Click "Create Domain" or "New Domain"'); |
| 60 | + console.log(' 3. Save the domain (e.g., acode-prettier.ngrok-free.app)'); |
| 61 | + console.log(' 4. Add to ngrok config: ngrok config edit'); |
| 62 | + console.log(' 5. Add under agent section:'); |
| 63 | + console.log(' domain: your-domain.ngrok-free.app\n'); |
| 64 | + } |
| 65 | + |
| 66 | + console.log('Copy the URL above and paste it in Acode:'); |
| 67 | + console.log('Settings → Plugins → + → REMOTE\n'); |
| 68 | + |
| 69 | + if (process.send) { |
| 70 | + process.send('OK'); |
| 71 | + } |
| 72 | + } catch (error) { |
| 73 | + console.error('\n❌ Error starting server:'); |
| 74 | + console.error(error.message); |
| 75 | + process.exit(1); |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +async function getNgrokAuthtoken() { |
| 80 | + // Try to read authtoken from ngrok config file |
| 81 | + for (const configPath of NGROK_CONFIG_PATHS) { |
| 82 | + if (fs.existsSync(configPath)) { |
| 83 | + const config = fs.readFileSync(configPath, 'utf8'); |
| 84 | + const match = config.match(/authtoken:\s*([^\s]+)/); |
| 85 | + if (match) { |
| 86 | + return match[1]; |
42 | 87 | } |
| 88 | + } |
| 89 | + } |
43 | 90 |
|
44 | | - next(); |
45 | | - }], |
46 | | - }); |
| 91 | + // If not found, show error |
| 92 | + console.error('\n❌ ngrok authtoken not configured!\n'); |
| 93 | + console.error('Please follow these steps:'); |
| 94 | + console.error('1. Sign up at https://dashboard.ngrok.com/signup'); |
| 95 | + console.error('2. Get your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken'); |
| 96 | + console.error('3. Run: ngrok config add-authtoken <YOUR_TOKEN>\n'); |
| 97 | + process.exit(1); |
| 98 | +} |
| 99 | + |
| 100 | +function getStaticDomain() { |
| 101 | + // Try to read static domain from ngrok config file |
| 102 | + for (const configPath of NGROK_CONFIG_PATHS) { |
| 103 | + if (fs.existsSync(configPath)) { |
| 104 | + const config = fs.readFileSync(configPath, 'utf8'); |
| 105 | + // Look for domain under agent section |
| 106 | + const match = config.match(/domain:\s*([^\s]+)/); |
| 107 | + if (match) { |
| 108 | + return match[1]; |
| 109 | + } |
| 110 | + } |
| 111 | + } |
47 | 112 |
|
48 | | - process.send('OK'); |
| 113 | + return null; |
49 | 114 | } |
50 | 115 |
|
51 | | -function sendFile(res, filePath) { |
52 | | - if (fs.existsSync(filePath)) { |
53 | | - const stat = fs.statSync(filePath); |
| 116 | +function handleRequest(req, res) { |
| 117 | + // Parse URL and remove query string |
| 118 | + const url = req.url.split('?')[0]; |
54 | 119 |
|
55 | | - res.writeHead(200, { |
56 | | - 'Content-Type': 'application/javascript', |
57 | | - 'Content-Length': stat.size, |
58 | | - }); |
| 120 | + // Determine file path |
| 121 | + let filePath; |
| 122 | + if (url === '/' || url === '/index.html') { |
| 123 | + filePath = path.join(PROJECT_ROOT, 'index.html'); |
| 124 | + } else { |
| 125 | + filePath = path.join(PROJECT_ROOT, url); |
| 126 | + } |
| 127 | + |
| 128 | + // Security check: ensure file is within project root |
| 129 | + const normalizedPath = path.normalize(filePath); |
| 130 | + if (!normalizedPath.startsWith(PROJECT_ROOT)) { |
| 131 | + res.writeHead(403, { 'Content-Type': 'text/plain' }); |
| 132 | + res.end('403 Forbidden'); |
| 133 | + return; |
| 134 | + } |
| 135 | + |
| 136 | + // Check if file exists |
| 137 | + if (!fs.existsSync(filePath)) { |
| 138 | + res.writeHead(404, { 'Content-Type': 'text/plain' }); |
| 139 | + res.end('404 Not Found'); |
| 140 | + return; |
| 141 | + } |
59 | 142 |
|
60 | | - const readStream = fs.createReadStream(filePath); |
61 | | - readStream.pipe(res); |
| 143 | + // Check if path is a directory |
| 144 | + const stat = fs.statSync(filePath); |
| 145 | + if (stat.isDirectory()) { |
| 146 | + res.writeHead(403, { 'Content-Type': 'text/plain' }); |
| 147 | + res.end('403 Forbidden: Directory listing not allowed'); |
62 | 148 | return; |
63 | 149 | } |
64 | 150 |
|
65 | | - res.writeHead(404, { 'Content-Type': 'text/plain' }); |
66 | | - res.end(`ERROR cannot get ${filePath}`); |
| 151 | + // Determine content type |
| 152 | + const ext = path.extname(filePath).toLowerCase(); |
| 153 | + const contentTypes = { |
| 154 | + '.html': 'text/html', |
| 155 | + '.js': 'application/javascript', |
| 156 | + '.json': 'application/json', |
| 157 | + '.zip': 'application/zip', |
| 158 | + '.css': 'text/css', |
| 159 | + '.png': 'image/png', |
| 160 | + '.jpg': 'image/jpeg', |
| 161 | + '.gif': 'image/gif', |
| 162 | + '.svg': 'image/svg+xml', |
| 163 | + '.txt': 'text/plain', |
| 164 | + }; |
| 165 | + const contentType = contentTypes[ext] || 'application/octet-stream'; |
| 166 | + |
| 167 | + // Serve file |
| 168 | + res.writeHead(200, { |
| 169 | + 'Content-Type': contentType, |
| 170 | + 'Content-Length': stat.size, |
| 171 | + 'Access-Control-Allow-Origin': '*', |
| 172 | + }); |
| 173 | + |
| 174 | + const readStream = fs.createReadStream(filePath); |
| 175 | + readStream.on('error', (err) => { |
| 176 | + console.error('Error reading file:', err); |
| 177 | + if (!res.headersSent) { |
| 178 | + res.writeHead(500, { 'Content-Type': 'text/plain' }); |
| 179 | + } |
| 180 | + res.end('500 Internal Server Error'); |
| 181 | + }); |
| 182 | + readStream.pipe(res); |
67 | 183 | } |
0 commit comments