Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Bump version
uses: phips28/gh-action-bump-version@v11.0.7
with:
tag-prefix: ""

- name: Build
run: npm run build

- name: Publish to NPM
run: npm publish --access public --provenance
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
# localview

Easily access a localhost website from your mobile device.
Open your local dev server on your phone with a QR scan.

## Usage
`localview` finds your machine's LAN IP, builds a URL pointing at your dev server's port, and prints a QR code in the terminal. Scan it from any device on the same network. No tunnels, no signup, no daemon.

1. Start your localhost server.
2. Create a QR code with localview.
![Demo](./static/demo.gif)

```bash
npx localview --port 8080
```
## Quick start

3. To visit the exposed URL, scan the QR code with your mobile device.
1. Start your dev server.
2. In a separate terminal, run `localview` with the same port:

![Demo](./static/demo.gif)
```bash
npx localview --port 8080
```

3. Scan the QR code with your phone's camera. The page opens in the default browser.

## Use cases

- **Mobile UI checks**: touch targets, breakpoints, virtual keyboards, and the OS chrome that desktop devtools approximations can't fully replicate.
- **Real-device APIs**: `getUserMedia`, geolocation, device orientation, `vibrate`, and other sensor or permission flows that only work on a real phone.
- **PWA install flow**: Add-to-home-screen prompts, splash screens, standalone-mode display.

## Options

| Flag | Description |
| -------------- | ------------------------------------------------------------- |
| `--port`, `-P` | Port exposed by the local server. **Required.** |
| `--path` | Path appended to the URL (e.g. `/admin`). Optional. |
| `--port`, `-P` | Port your dev server is bound to. **Required.** |
| `--path` | Path appended to the URL (e.g. `/admin`). |
| `--host` | Override the auto-detected LAN IP (Docker, multi-NIC, demos). |

### Examples
Expand All @@ -33,13 +41,13 @@ npx localview --port 3000 --path /admin
npx localview --port 8080 --host 192.168.1.42
```

If multiple non-internal IPv4 interfaces are detected (e.g. wifi + VPN + Docker bridges), localview prints a numbered list and asks which one to use; press Enter to accept the smart default.
If your machine has multiple LAN interfaces (wifi + VPN + Docker bridges, for example), `localview` shows an interactive picker with arrow-key navigation. The smart-sorted default (wifi and ethernet first) is selected by pressing Enter.

## Notes
## Requirements

- Requires Node.js `^20.19.0`, `^22.12.0`, or `>=23`.
- Both the server and the mobile device need to be connected to the same network.
- You may need to set your server's host to `0.0.0.0`.
- Node.js `^20.19.0`, `^22.12.0`, or `>=23`.
- Your dev server and your phone connected to the same network.
- Your dev server must accept connections from its LAN address. Many frameworks bind to `127.0.0.1` only by default; bind to `0.0.0.0` instead (the exact flag depends on your tool: `--host 0.0.0.0`, `--bind 0.0.0.0`, `HOST=0.0.0.0`, etc.) to expose it on the LAN.

## License

Expand Down
59 changes: 59 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"start": "tsup src/cli.ts --format cjs --watch"
},
"dependencies": {
"@clack/prompts": "^1.3.0",
"qrcode-terminal": "^0.12.0",
"yargs": "^18.0.0"
},
Expand Down
39 changes: 13 additions & 26 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import os from "os";
import readline from "readline";
import { isCancel, select } from "@clack/prompts";
import yargs from "yargs/yargs";
import { hideBin } from "yargs/helpers";
import qrcode from "qrcode-terminal";
Expand Down Expand Up @@ -74,34 +74,21 @@ async function pickAddress(): Promise<string | null> {
return first.address;
}

console.log("Multiple network interfaces detected:");
candidates.forEach((c, i) => {
const label = c.kind === "other" ? "" : ` (${c.kind})`;
console.log(` ${i + 1}. ${c.name}${label} — ${c.address}`);
const selected = await select({
message: "Select network interface",
initialValue: candidates[0].address,
options: candidates.map((c) => {
const label = c.kind === "other" ? c.name : `${c.name} (${c.kind})`;
return { value: c.address, label, hint: c.address };
}),
});
const answer = await prompt(
`Select interface [1-${candidates.length}] (default: 1): `,
);
const trimmed = answer.trim();
const choice = trimmed === "" ? 1 : Number(trimmed);
if (!Number.isInteger(choice) || choice < 1 || choice > candidates.length) {
console.log("Invalid selection.");
process.exit(1);

if (isCancel(selected)) {
console.log("Cancelled.");
process.exit(0);
}
return candidates[choice - 1].address;
}

function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
return selected as string;
}

function normalizePath(value: string): string {
Expand Down
Binary file modified static/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.