localpeek 1.0.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/GUIDE.md +128 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/bin/localpeek.js +19 -0
- package/package.json +51 -0
- package/src/adapters/astro.js +27 -0
- package/src/adapters/generic.js +38 -0
- package/src/adapters/html.js +97 -0
- package/src/adapters/next.js +28 -0
- package/src/adapters/vite.js +30 -0
- package/src/cli.js +310 -0
- package/src/detect.js +193 -0
- package/src/errors.js +81 -0
- package/src/logger.js +208 -0
- package/src/network.js +113 -0
- package/src/port.js +65 -0
- package/src/qr.js +21 -0
- package/src/server.js +130 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { detectProject } = require('./detect.js');
|
|
5
|
+
const { getBestLanAddress, getLanCandidates } = require('./network.js');
|
|
6
|
+
const { isPortFree, findFreePort, probeExistingServer } = require('./port.js');
|
|
7
|
+
const { printQr } = require('./qr.js');
|
|
8
|
+
const {
|
|
9
|
+
spawnDevServer,
|
|
10
|
+
startStaticServer,
|
|
11
|
+
waitUntilListening,
|
|
12
|
+
defaultPortFor,
|
|
13
|
+
} = require('./server.js');
|
|
14
|
+
const logger = require('./logger.js');
|
|
15
|
+
const { LocalPeekError } = require('./errors.js');
|
|
16
|
+
|
|
17
|
+
const VERSION = require('../package.json').version;
|
|
18
|
+
const AUTHOR = 'Dipto Thakur';
|
|
19
|
+
|
|
20
|
+
const HELP_TEXT = `
|
|
21
|
+
LocalPeek — open your local dev project on your phone over LAN
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
localpeek [options]
|
|
25
|
+
lp [options]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
-p, --port <number> Port to run on (default: framework's usual port)
|
|
29
|
+
-i, --iface <name> Prefer a specific network interface (e.g. en0, Wi-Fi)
|
|
30
|
+
-d, --dir <path> Project directory (default: current directory)
|
|
31
|
+
-v, --version Print the version number
|
|
32
|
+
-h, --help Show this help message
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
localpeek
|
|
36
|
+
localpeek --port 5000
|
|
37
|
+
npx localpeek
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const opts = { port: null, iface: null, dir: process.cwd(), help: false, version: false };
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const arg = argv[i];
|
|
45
|
+
switch (arg) {
|
|
46
|
+
case '-p':
|
|
47
|
+
case '--port':
|
|
48
|
+
opts.port = Number(argv[++i]);
|
|
49
|
+
break;
|
|
50
|
+
case '-i':
|
|
51
|
+
case '--iface':
|
|
52
|
+
opts.iface = argv[++i];
|
|
53
|
+
break;
|
|
54
|
+
case '-d':
|
|
55
|
+
case '--dir':
|
|
56
|
+
opts.dir = path.resolve(argv[++i]);
|
|
57
|
+
break;
|
|
58
|
+
case '-v':
|
|
59
|
+
case '--version':
|
|
60
|
+
opts.version = true;
|
|
61
|
+
break;
|
|
62
|
+
case '-h':
|
|
63
|
+
case '--help':
|
|
64
|
+
opts.help = true;
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
// CAVEMAN: Unknown flags are ignored rather than crashing —
|
|
68
|
+
// this stays a small, forgiving tool.
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return opts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printFriendlyError(err) {
|
|
77
|
+
logger.blank();
|
|
78
|
+
logger.error(err.message);
|
|
79
|
+
if (err.hint) {
|
|
80
|
+
logger.info(logger.colors.dim(err.hint));
|
|
81
|
+
}
|
|
82
|
+
logger.blank();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function run(argv) {
|
|
86
|
+
const opts = parseArgs(argv);
|
|
87
|
+
|
|
88
|
+
if (opts.help) {
|
|
89
|
+
logger.raw(HELP_TEXT);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (opts.version) {
|
|
94
|
+
logger.raw(VERSION);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.banner(VERSION, AUTHOR);
|
|
99
|
+
|
|
100
|
+
// ---- Detect project -------------------------------------------------
|
|
101
|
+
const detectSpinner = logger.createSpinner('Detecting project...').start();
|
|
102
|
+
let project;
|
|
103
|
+
try {
|
|
104
|
+
project = detectProject(opts.dir);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
detectSpinner.stop();
|
|
107
|
+
if (err instanceof LocalPeekError) {
|
|
108
|
+
printFriendlyError(err);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
detectSpinner.stop(`Detected ${logger.colors.bold(project.type)} project`);
|
|
115
|
+
|
|
116
|
+
// ---- Find LAN address -------------------------------------------------
|
|
117
|
+
const lanSpinner = logger.createSpinner('Finding LAN address...').start();
|
|
118
|
+
let lan;
|
|
119
|
+
try {
|
|
120
|
+
lan = getBestLanAddress(opts.iface);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
lanSpinner.stop();
|
|
123
|
+
if (err instanceof LocalPeekError) {
|
|
124
|
+
printFriendlyError(err);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
const candidates = getLanCandidates();
|
|
131
|
+
lanSpinner.stop(`Found network address ${logger.colors.dim(`(${lan.iface})`)}`);
|
|
132
|
+
|
|
133
|
+
// ---- Resolve port -------------------------------------------------
|
|
134
|
+
const desiredPort = opts.port || defaultPortFor(project.type);
|
|
135
|
+
const portSpinner = logger.createSpinner(`Checking port ${desiredPort}...`).start();
|
|
136
|
+
|
|
137
|
+
const portFree = await isPortFree(desiredPort);
|
|
138
|
+
let port = desiredPort;
|
|
139
|
+
let reusingExisting = false;
|
|
140
|
+
|
|
141
|
+
if (portFree) {
|
|
142
|
+
portSpinner.stop(`Port ${port} is free`);
|
|
143
|
+
} else {
|
|
144
|
+
const looksAlive = await probeExistingServer(desiredPort);
|
|
145
|
+
if (looksAlive && !opts.port) {
|
|
146
|
+
// CAVEMAN: Something is already serving on the expected port.
|
|
147
|
+
// Reuse it instead of starting a duplicate process — but only
|
|
148
|
+
// when the user didn't explicitly ask for this exact port
|
|
149
|
+
// (an explicit --port request means "use this port or tell me
|
|
150
|
+
// why not", not "silently reuse whatever's there").
|
|
151
|
+
reusingExisting = true;
|
|
152
|
+
port = desiredPort;
|
|
153
|
+
portSpinner.stop(`Server already running on ${port} — reusing it`);
|
|
154
|
+
} else {
|
|
155
|
+
const found = await findFreePort(desiredPort + 1);
|
|
156
|
+
if (!found) {
|
|
157
|
+
portSpinner.stop();
|
|
158
|
+
printFriendlyError(
|
|
159
|
+
new LocalPeekError(`Could not find a free port near ${desiredPort}`, {
|
|
160
|
+
hint: 'Free up some ports, or pass --port <number> to choose one directly.',
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
port = found;
|
|
167
|
+
portSpinner.stop(`Port ${desiredPort} was busy — using ${port} instead`, 'warn');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const host = '0.0.0.0';
|
|
172
|
+
const mobileUrl = `http://${lan.address}:${port}`;
|
|
173
|
+
const localUrl = `http://localhost:${port}`;
|
|
174
|
+
|
|
175
|
+
let stopFn = null;
|
|
176
|
+
|
|
177
|
+
if (!reusingExisting) {
|
|
178
|
+
try {
|
|
179
|
+
if (project.type === 'html') {
|
|
180
|
+
const serveSpinner = logger.createSpinner('Starting static server...').start();
|
|
181
|
+
const httpServer = await startStaticServer(project.dir, port, host);
|
|
182
|
+
stopFn = () => httpServer.close();
|
|
183
|
+
serveSpinner.stop('Static server ready');
|
|
184
|
+
} else {
|
|
185
|
+
// CAVEMAN: The dev server's own output is inherited straight
|
|
186
|
+
// to this terminal (stdio:'inherit'), so a spinner here would
|
|
187
|
+
// fight with it for the same line. Print a plain marker
|
|
188
|
+
// instead and let the framework's own logs speak.
|
|
189
|
+
logger.info(logger.colors.dim(`Starting dev server (${project.adapter})...`));
|
|
190
|
+
logger.blank();
|
|
191
|
+
const { stop } = spawnDevServer(project, port, host);
|
|
192
|
+
stopFn = stop;
|
|
193
|
+
|
|
194
|
+
const ready = await waitUntilListening(port);
|
|
195
|
+
logger.blank();
|
|
196
|
+
if (!ready) {
|
|
197
|
+
logger.warn(
|
|
198
|
+
'The dev server is taking a while to start. LocalPeek will keep waiting — check the output above for errors.'
|
|
199
|
+
);
|
|
200
|
+
} else {
|
|
201
|
+
logger.success('Dev server is up');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err instanceof LocalPeekError) {
|
|
206
|
+
printFriendlyError(err);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await printSummary({
|
|
215
|
+
project,
|
|
216
|
+
localUrl,
|
|
217
|
+
mobileUrl,
|
|
218
|
+
port,
|
|
219
|
+
lan,
|
|
220
|
+
candidates,
|
|
221
|
+
reusingExisting,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
installShutdownHandlers(stopFn);
|
|
225
|
+
|
|
226
|
+
// Keep the process alive. For spawned children, stdio:'inherit'
|
|
227
|
+
// keeps the terminal attached already; for the static server we
|
|
228
|
+
// just idle until Ctrl+C.
|
|
229
|
+
if (project.type === 'html') {
|
|
230
|
+
await new Promise(() => {}); // eslint-disable-line no-unused-vars
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function printSummary({ project, localUrl, mobileUrl, port, lan, candidates, reusingExisting }) {
|
|
235
|
+
const { colors } = logger;
|
|
236
|
+
const qrString = await printQr(mobileUrl);
|
|
237
|
+
|
|
238
|
+
logger.blank();
|
|
239
|
+
logger.box(
|
|
240
|
+
[
|
|
241
|
+
`${colors.dim('Local')} ${colors.dim(localUrl)}`,
|
|
242
|
+
`${colors.dim('Network')} ${colors.boldWhite(mobileUrl)}`,
|
|
243
|
+
],
|
|
244
|
+
{ title: 'Ready' }
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
logger.blank();
|
|
248
|
+
logger.info('Scan with your phone:');
|
|
249
|
+
logger.blank();
|
|
250
|
+
logger.raw(qrString);
|
|
251
|
+
logger.blank();
|
|
252
|
+
|
|
253
|
+
logger.line('Framework:', project.type);
|
|
254
|
+
logger.line('Port:', String(port));
|
|
255
|
+
logger.line('Interface:', `${lan.iface} (${lan.address})`);
|
|
256
|
+
logger.blank();
|
|
257
|
+
|
|
258
|
+
logger.info(colors.bold('Next steps'));
|
|
259
|
+
logger.step(1, 'Make sure your phone is on the same Wi-Fi network.');
|
|
260
|
+
logger.step(2, `Open ${colors.cyan(mobileUrl)} on your phone, or scan the QR code above.`);
|
|
261
|
+
logger.step(3, 'Edit your project — changes show up like normal.');
|
|
262
|
+
logger.blank();
|
|
263
|
+
|
|
264
|
+
if (reusingExisting) {
|
|
265
|
+
logger.info(
|
|
266
|
+
colors.dim(`Reused the server already running on port ${port} instead of starting a new one.`)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (candidates.length > 1) {
|
|
271
|
+
logger.info(
|
|
272
|
+
colors.dim(
|
|
273
|
+
`Multiple network interfaces found — using ${lan.iface}. Pass --iface <name> to pick another.`
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (reusingExisting || candidates.length > 1) logger.blank();
|
|
279
|
+
|
|
280
|
+
logger.info(
|
|
281
|
+
colors.dim("Phone can't connect? Check you're on the same network, no VPN, and the port isn't firewalled.")
|
|
282
|
+
);
|
|
283
|
+
logger.blank();
|
|
284
|
+
logger.info(colors.dim(`${logger.symbols.bullet} Press `) + colors.bold('Ctrl+C') + colors.dim(' to stop.'));
|
|
285
|
+
logger.blank();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function installShutdownHandlers(stopFn) {
|
|
289
|
+
let shuttingDown = false;
|
|
290
|
+
const shutdown = () => {
|
|
291
|
+
if (shuttingDown) return;
|
|
292
|
+
shuttingDown = true;
|
|
293
|
+
logger.blank();
|
|
294
|
+
logger.info('Stopping LocalPeek...');
|
|
295
|
+
if (stopFn) {
|
|
296
|
+
try {
|
|
297
|
+
stopFn();
|
|
298
|
+
} catch {
|
|
299
|
+
// CAVEMAN: Best-effort shutdown. If the child is already
|
|
300
|
+
// gone, there's nothing more to do.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
process.exit(0);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
process.on('SIGINT', shutdown);
|
|
307
|
+
process.on('SIGTERM', shutdown);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { run, parseArgs };
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { NoProjectError, MissingDependenciesError } = require('./errors.js');
|
|
6
|
+
|
|
7
|
+
// CAVEMAN: We only ever READ files here. Never write, never modify
|
|
8
|
+
// the user's project.
|
|
9
|
+
|
|
10
|
+
function readJsonSafe(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function exists(p) {
|
|
19
|
+
try {
|
|
20
|
+
fs.accessSync(p);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findFirstExisting(dir, names) {
|
|
28
|
+
return names.find((n) => exists(path.join(dir, n))) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Order matters: check more specific frameworks before generic ones.
|
|
32
|
+
// Each entry: { type, adapter, test(pkg, dir) }
|
|
33
|
+
const FRAMEWORK_TESTS = [
|
|
34
|
+
{
|
|
35
|
+
type: 'next',
|
|
36
|
+
adapter: 'next',
|
|
37
|
+
test: (pkg, dir) =>
|
|
38
|
+
hasDep(pkg, 'next') || findFirstExisting(dir, ['next.config.js', 'next.config.mjs', 'next.config.ts']),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'astro',
|
|
42
|
+
adapter: 'astro',
|
|
43
|
+
test: (pkg, dir) =>
|
|
44
|
+
hasDep(pkg, 'astro') || findFirstExisting(dir, ['astro.config.mjs', 'astro.config.js', 'astro.config.ts']),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'sveltekit',
|
|
48
|
+
adapter: 'generic',
|
|
49
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
50
|
+
test: (pkg) => hasDep(pkg, '@sveltejs/kit'),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: 'svelte',
|
|
54
|
+
adapter: 'generic',
|
|
55
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
56
|
+
test: (pkg, dir) =>
|
|
57
|
+
hasDep(pkg, 'svelte') || findFirstExisting(dir, ['svelte.config.js']),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'nuxt',
|
|
61
|
+
adapter: 'generic',
|
|
62
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
63
|
+
test: (pkg, dir) =>
|
|
64
|
+
hasDep(pkg, 'nuxt') || hasDep(pkg, 'nuxt3') || findFirstExisting(dir, ['nuxt.config.js', 'nuxt.config.ts']),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'vue',
|
|
68
|
+
adapter: 'generic',
|
|
69
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
70
|
+
test: (pkg, dir) =>
|
|
71
|
+
(hasDep(pkg, 'vue') && hasDep(pkg, 'vite')) || findFirstExisting(dir, ['vue.config.js']),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'remix',
|
|
75
|
+
adapter: 'generic',
|
|
76
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
77
|
+
test: (pkg) => hasDep(pkg, '@remix-run/dev') || hasDep(pkg, '@remix-run/react'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'angular',
|
|
81
|
+
adapter: 'generic',
|
|
82
|
+
defaultFlags: ['--host', '0.0.0.0'],
|
|
83
|
+
test: (pkg, dir) => hasDep(pkg, '@angular/core') || exists(path.join(dir, 'angular.json')),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'vite',
|
|
87
|
+
adapter: 'vite',
|
|
88
|
+
test: (pkg, dir) =>
|
|
89
|
+
hasDep(pkg, 'vite') ||
|
|
90
|
+
findFirstExisting(dir, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function hasDep(pkg, name) {
|
|
95
|
+
if (!pkg) return false;
|
|
96
|
+
return Boolean(
|
|
97
|
+
(pkg.dependencies && pkg.dependencies[name]) ||
|
|
98
|
+
(pkg.devDependencies && pkg.devDependencies[name])
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function looksLikeStaticHtmlProject(dir) {
|
|
103
|
+
return exists(path.join(dir, 'index.html'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Detects the project in `dir`.
|
|
108
|
+
* Returns:
|
|
109
|
+
* {
|
|
110
|
+
* type: 'html' | 'vite' | 'next' | 'astro' | 'generic',
|
|
111
|
+
* adapter: same set of names,
|
|
112
|
+
* dir,
|
|
113
|
+
* pkg: parsed package.json or null,
|
|
114
|
+
* hasPackageJson: bool,
|
|
115
|
+
* devScript: string|null // pkg.scripts.dev if present
|
|
116
|
+
* defaultFlags: string[] // extra flags a generic adapter should try
|
|
117
|
+
* nodeModulesInstalled: bool
|
|
118
|
+
* }
|
|
119
|
+
* Throws NoProjectError / MissingDependenciesError when nothing usable found.
|
|
120
|
+
*/
|
|
121
|
+
function detectProject(dir = process.cwd()) {
|
|
122
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
123
|
+
const hasPackageJson = exists(pkgPath);
|
|
124
|
+
const pkg = hasPackageJson ? readJsonSafe(pkgPath) : null;
|
|
125
|
+
|
|
126
|
+
if (hasPackageJson && pkg === null) {
|
|
127
|
+
throw new NoProjectError(dir);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nodeModulesInstalled = exists(path.join(dir, 'node_modules'));
|
|
131
|
+
|
|
132
|
+
if (hasPackageJson) {
|
|
133
|
+
if (!nodeModulesInstalled) {
|
|
134
|
+
// A package.json exists, declares real dependencies, but
|
|
135
|
+
// nothing is installed. Don't guess — ask the user to install.
|
|
136
|
+
const declaresDeps =
|
|
137
|
+
(pkg.dependencies && Object.keys(pkg.dependencies).length) ||
|
|
138
|
+
(pkg.devDependencies && Object.keys(pkg.devDependencies).length);
|
|
139
|
+
if (declaresDeps) {
|
|
140
|
+
throw new MissingDependenciesError();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const fw of FRAMEWORK_TESTS) {
|
|
145
|
+
if (fw.test(pkg, dir)) {
|
|
146
|
+
return {
|
|
147
|
+
type: fw.type,
|
|
148
|
+
adapter: fw.adapter,
|
|
149
|
+
dir,
|
|
150
|
+
pkg,
|
|
151
|
+
hasPackageJson,
|
|
152
|
+
devScript: (pkg.scripts && pkg.scripts.dev) || null,
|
|
153
|
+
defaultFlags: fw.defaultFlags || [],
|
|
154
|
+
nodeModulesInstalled,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Has a package.json but no framework matched. If there's a
|
|
160
|
+
// "dev" script, trust it and use the generic adapter.
|
|
161
|
+
if (pkg.scripts && pkg.scripts.dev) {
|
|
162
|
+
return {
|
|
163
|
+
type: 'generic',
|
|
164
|
+
adapter: 'generic',
|
|
165
|
+
dir,
|
|
166
|
+
pkg,
|
|
167
|
+
hasPackageJson,
|
|
168
|
+
devScript: pkg.scripts.dev,
|
|
169
|
+
defaultFlags: [],
|
|
170
|
+
nodeModulesInstalled,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// No package.json (or no framework/dev script match) — fall
|
|
176
|
+
// back to plain static HTML if an index.html exists.
|
|
177
|
+
if (looksLikeStaticHtmlProject(dir)) {
|
|
178
|
+
return {
|
|
179
|
+
type: 'html',
|
|
180
|
+
adapter: 'html',
|
|
181
|
+
dir,
|
|
182
|
+
pkg,
|
|
183
|
+
hasPackageJson,
|
|
184
|
+
devScript: null,
|
|
185
|
+
defaultFlags: [],
|
|
186
|
+
nodeModulesInstalled,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new NoProjectError(dir);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { detectProject, hasDep, exists };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CAVEMAN: Central place for "known" errors so cli.js can print
|
|
4
|
+
// a friendly one-liner instead of a raw stack trace. Anything not
|
|
5
|
+
// wrapped in one of these classes is treated as unexpected.
|
|
6
|
+
|
|
7
|
+
class LocalPeekError extends Error {
|
|
8
|
+
constructor(message, { hint } = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'LocalPeekError';
|
|
11
|
+
this.hint = hint || null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class NoProjectError extends LocalPeekError {
|
|
16
|
+
constructor(dir) {
|
|
17
|
+
super(`No project found in ${dir}`, {
|
|
18
|
+
hint:
|
|
19
|
+
"Run LocalPeek from inside a project folder (one with an index.html or package.json).",
|
|
20
|
+
});
|
|
21
|
+
this.name = 'NoProjectError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class NoDevCommandError extends LocalPeekError {
|
|
26
|
+
constructor(details) {
|
|
27
|
+
super('Could not figure out how to start this project', {
|
|
28
|
+
hint:
|
|
29
|
+
details ||
|
|
30
|
+
'Add a "dev" script to package.json, or run LocalPeek from a folder with an index.html file.',
|
|
31
|
+
});
|
|
32
|
+
this.name = 'NoDevCommandError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class MissingDependenciesError extends LocalPeekError {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('This project has a package.json but no installed dependencies', {
|
|
39
|
+
hint: 'Run your package manager\'s install command first (for example: npm install), then run LocalPeek again.',
|
|
40
|
+
});
|
|
41
|
+
this.name = 'MissingDependenciesError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class NoLanInterfaceError extends LocalPeekError {
|
|
46
|
+
constructor() {
|
|
47
|
+
super('No usable LAN network address was found on this computer', {
|
|
48
|
+
hint:
|
|
49
|
+
'Connect to Wi-Fi or Ethernet on the same network as your phone, then try again. VPNs and some virtual adapters can hide the real address.',
|
|
50
|
+
});
|
|
51
|
+
this.name = 'NoLanInterfaceError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class PortInUseError extends LocalPeekError {
|
|
56
|
+
constructor(port) {
|
|
57
|
+
super(`Port ${port} is already in use`, {
|
|
58
|
+
hint: 'Free the port, or let LocalPeek pick a different one automatically.',
|
|
59
|
+
});
|
|
60
|
+
this.name = 'PortInUseError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ServerStartError extends LocalPeekError {
|
|
65
|
+
constructor(reason) {
|
|
66
|
+
super(`The development server failed to start${reason ? `: ${reason}` : ''}`, {
|
|
67
|
+
hint: 'Try running your normal dev command directly to see the full error.',
|
|
68
|
+
});
|
|
69
|
+
this.name = 'ServerStartError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
LocalPeekError,
|
|
75
|
+
NoProjectError,
|
|
76
|
+
NoDevCommandError,
|
|
77
|
+
MissingDependenciesError,
|
|
78
|
+
NoLanInterfaceError,
|
|
79
|
+
PortInUseError,
|
|
80
|
+
ServerStartError,
|
|
81
|
+
};
|