Skip to content

Commit

Permalink
feat: listhen cli (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Jul 18, 2023
1 parent 174e9fe commit 4bbcb7f
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 54 deletions.
70 changes: 39 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ An elegant HTTP listener.
[![License][license-src]][license-href]
[![JSDocs][jsdocs-src]][jsdocs-href]

## Features
## Features

- Built-in CLI To run your applications with watch mode and typescript support (with [unjs/jiti](https://github.com/unjs/jiti))
- Promisified interface for listening and closing server
- Work with express/connect or plain http handle function
- Support HTTP and HTTPS
- Assign a port or fallback to human friendly alternative (with [get-port-please](https://github.com/unjs/get-port-please))
- Assign a port or fallback to human friendly alternative (with [unjs/get-port-please](https://github.com/unjs/get-port-please))
- Generate listening URL and show on console
- Copy URL to clipboard (dev only by default)
- Open URL in browser (opt-in)
Expand All @@ -22,51 +23,57 @@ An elegant HTTP listener.
- Close on exit signal
- Gracefully shutdown server with [http-shutdown](https://github.com/thedillonb/http-shutdown)

## Install
## Quick Usage (CLI)

Install using npm:
You can run your applications in localhost with typescript support and watch mode using `listhen` CLI:

```bash
npm i listhen
```
Create `app.ts`:

Import into your Node.js project:
```ts
export default (req, res) => {
res.end("Hello World!");
};
```

```js
// CommonJS
const { listen } = require('listhen')
Use npx to invoke `listhen` command:

// ESM
import { listen } from 'listhen'
```sh
npx listhen -w ./app.ts
```

## Usage
## Usage (API)

**Function signature:**
Install package:

```ts
const { url, getURL, server, close } = await listen(handle, options?)
```
```bash
# pnpm
pnpm i listhen

**Plain handle function:**
# npm
npm i listhen

# yarn
yarn add listhen

```ts
listen((_req, res) => {
res.end('hi')
})
```

**With express/connect:**
Import into your Node.js project:

```ts
const express = require('express')
const app = express()
```js
// CommonJS
const { listen, listenAndWatch } = require("listhen");

// ESM
import { listen, listenAndWatch } from "listhen";
```

app.use('/', ((_req, res) => {
res.end('hi')
})
```ts
const handler = (req, res) => {
res.end("Hi!")
}

listen(app)
// listener: { url, getURL, server, close, ... }
const listener = await listen(handle, options?)
```
## Options
Expand Down Expand Up @@ -144,6 +151,7 @@ Automatically close when an exit signal is received on process.
MIT. Made with 💖
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/listhen?style=flat&colorA=18181B&colorB=F0DB4F
[npm-version-href]: https://npmjs.com/package/listhen
[npm-downloads-src]: https://img.shields.io/npm/dm/listhen?style=flat&colorA=18181B&colorB=F0DB4F
Expand Down
5 changes: 5 additions & 0 deletions bin/listhen.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { runMain } from "../dist/cli.mjs";

runMain();
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
{
"name": "listhen",
"version": "1.0.4",
"description": "",
"description": "👂 Elegant HTTP Listener",
"repository": "unjs/listhen",
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./cli": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.mjs",
"require": "./dist/cli.cjs"
}
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"listen": "./dist/cli.cjs",
"listhen": "./dist/cli.cjs"
},
"files": [
"dist",
"lib"
Expand All @@ -27,6 +36,7 @@
"test": "pnpm lint && vitest run --coverage"
},
"dependencies": {
"citty": "^0.1.2",
"clipboardy": "^3.0.0",
"consola": "^3.2.3",
"defu": "^6.1.2",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

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

26 changes: 26 additions & 0 deletions src/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { promises as fs } from "node:fs";
import { networkInterfaces } from "node:os";
import { relative, resolve } from "node:path";
import { colors } from "consola/utils";
import { fileURLToPath } from "mlly";
import type { Certificate, HTTPSOptions } from "./types";

export async function resolveCert(
Expand Down Expand Up @@ -66,3 +68,27 @@ export function formatURL(url: string) {
),
);
}

export async function createImporter(input: string, _cwd?: string) {
const cwd = resolve(_cwd ? fileURLToPath(_cwd) : ".");

const jiti = await import("jiti").then((r) => r.default || r);
const _jitiRequire = jiti(cwd, {
esmResolve: true,
requireCache: false,
interopDefault: true,
});

const entry = _jitiRequire.resolve(input);

const _import = () => {
const r = _jitiRequire(input);
return Promise.resolve(r.default || r);
};

return {
entry,
relativeEntry: relative(cwd, entry),
import: _import,
};
}
91 changes: 91 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { resolve } from "node:path";
import { defineCommand, runMain as _runMain } from "citty";
import { name, description, version } from "../package.json";
import { listen } from "./listen";
import { listenAndWatch } from "./watch";
import type { ListenOptions, WatchOptions } from "./types";
import { createImporter } from "./_utils";

export const main = defineCommand({
meta: {
name,
description,
version,
},
args: {
cwd: {
type: "string",
description: "Current working directory",
},
entry: {
type: "positional",
description: "Listener entry file (./app.ts)",
required: true,
},
port: {
type: "string",
description:
"Port to listen on (use PORT environment variable to override)",
},
host: {
type: "string",
description:
"Host to listen on (use HOST environment variable to override)",
},
clipboard: {
type: "boolean",
description: "Copy the URL to the clipboard",
default: false,
},
open: {
type: "boolean",
description: "Open the URL in the browser",
default: false,
},
baseURL: {
type: "string",
description: "Base URL to use",
},
name: {
type: "string",
description: "Name to use in the banner",
},
https: {
type: "boolean",
description: "Enable HTTPS",
default: false,
},
watch: {
type: "boolean",
description: "Watch for changes",
alias: "w",
default: false,
},
},
async run({ args }) {
const cwd = resolve(args.cwd || ".");
process.chdir(cwd);

const opts: Partial<ListenOptions & WatchOptions> = {
...args,
cwd,
port: args.port,
hostname: args.host,
clipboard: args.clipboard,
open: args.open,
baseURL: args.baseURL,
name: args.name,
https: args.https, // TODO: Support custom cert
};

if (args.watch) {
await listenAndWatch(args.entry, opts);
} else {
const importer = await createImporter(args.entry);
const handler = await importer.import();
await listen(handler, opts);
}
},
});

export const runMain = () => _runMain(main);
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Server } from "node:http";
import type { Server as HTTPServer } from "node:https";
import type { GetPortInput } from "get-port-please";
import type { ConsolaInstance } from "consola";

export interface Certificate {
key: string;
Expand Down Expand Up @@ -32,6 +33,7 @@ export interface ListenOptions {
export interface WatchOptions {
cwd: string;
entry: string;
logger: ConsolaInstance;
}

export interface ShowURLOptions {
Expand Down
27 changes: 11 additions & 16 deletions src/watch.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
import type { RequestListener } from "node:http";
import { resolve } from "node:path";
import { watch } from "node:fs";
import { fileURLToPath } from "mlly";
import { consola } from "consola";
import type { Listener, ListenOptions, WatchOptions } from "./types";
import { listen } from "./listen";
import { createImporter } from "./_utils";

export async function listenAndWatch(
input: string,
options: Partial<ListenOptions & WatchOptions> = {},
): Promise<Listener> {
const cwd = resolve(options.cwd ? fileURLToPath(options.cwd) : ".");

const jiti = await import("jiti").then((r) => r.default || r);
const _jitiRequire = jiti(cwd, {
esmResolve: true,
requireCache: false,
interopDefault: true,
});

const entry = _jitiRequire.resolve(input);
const logger = options.logger || consola.withTag("listhen");

let handle: RequestListener;

const resolveHandle = () => {
const imported = _jitiRequire(entry);
handle = imported.default || imported;
const importer = await createImporter(input);

const resolveHandle = async () => {
handle = await importer.import();
};

resolveHandle();

const watcher = await watch(entry, () => {
const watcher = await watch(importer.entry, () => {
logger.info(`\`${importer.relativeEntry}\` changed, Reloading...`);
resolveHandle();
});

const listenter = await listen((...args) => {
return handle(...args);
}, options);

logger.info(`Watching \`${importer.relativeEntry}\` for changes.`);

const _close = listenter.close;
listenter.close = async () => {
watcher.close();
Expand Down
9 changes: 3 additions & 6 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
"outDir": "dist",
"strict": true,
"declaration": true,
"types": [
"node"
]
"resolveJsonModule": true,
"types": ["node"]
},
"include": [
"src"
]
"include": ["src"]
}

0 comments on commit 4bbcb7f

Please sign in to comment.