Skip to content

Commit

Permalink
Add esm-cli package
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Dec 9, 2023
1 parent 9eee2e9 commit 48bfa36
Show file tree
Hide file tree
Showing 30 changed files with 552 additions and 438 deletions.
57 changes: 57 additions & 0 deletions packages/esm-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# esm.sh

The [esm.sh](https://esm.sh) CLI/API for serving hot applications.

## Using the CLI tool

The CLI tool is used to run a hot application in current directory.

```bash
npx esm.sh -w
```

> The `-w` option is for watching the file changes to enable HMR.
## Using the API

The esm.sh API uses standard web APIs to serve hot applications.

```ts
export interface ServeOptions {
/** The root path, default to current working directory. */
root?: string;
/** The fallback route, default is `index.html`. */
fallback?: string;
/** Wtaching file changes for HMR, default is `false` */
watch?: boolean;
}

export function serveHost(
options?: ServeOptions,
): (req: Request) => Promise<Response>;
```

For Node.js runtime, you need `@hono/server` to listen to the requests.

```js
import { serve } from "@hono/server";
import { serveHot } from "esm.sh";

serve({ port: 3000, fetch: serveHot() });
```

For Deno runtime, you can use `serveHot` directly.

```js
import { serveHot } from "https://esm.sh";

Deno.server(serveHot());
```

For Bun runtime:

```js
import { serveHot } from "esm.sh";

Bun.serve({ port: 3000, fetch: serveHot() });
```
28 changes: 11 additions & 17 deletions packages/esm-hot/bin/cli.mjs → packages/esm-cli/bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
#!/usr/bin/env node

import { serve } from "@hono/node-server";
import { existsSync } from "node:fs";
import { serve } from "../vendor/[email protected]";
import { serveHot } from "../src/index.mjs";

if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
Usage: npx @esm.sh/hot [options]
Usage: npx esm.sh [options] [root]
Options:
--cwd Current working directory (default: ".")
--help, -h Show help
--help, -h Show help message
--host Host to listen on (default: "localhost")
--plugins Plugins for service worker (default: [])
--port, -p Port number to listen on (default: 3000)
--spa Enable SPA mode
--watch Watch file changes for HMR
--watch, -w Watch file changes for HMR
`);
process.exit(0);
}
Expand All @@ -24,6 +22,12 @@ const args = {
};

process.argv.slice(2).forEach((arg) => {
if (!arg.startsWith("-")) {
if (existsSync(arg)) {
args.root = arg;
}
return;
}
const [key, value] = arg.split("=");
if ((key === "--port" || key === "-p") && value) {
args.port = parseInt(value);
Expand All @@ -32,18 +36,8 @@ process.argv.slice(2).forEach((arg) => {
}
} else if (key === "--host" && value) {
args.host = value;
} else if (key === "--spa") {
if (value) {
args.spa = { index: value };
} else {
args.spa = true;
}
} else if (key === "--watch" || key === "-w") {
args.watch = true;
} else if (key === "--cwd" && value) {
args.cwd = value;
} else if (key === "--plugins" && value) {
args.plugins = value.split(",");
}
});

Expand Down
35 changes: 35 additions & 0 deletions packages/esm-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "esm.sh",
"version": "0.135.0",
"description": "The CLI tool for esm.sh hot applications.",
"type": "module",
"main": "src/index.mjs",
"module": "src/index.mjs",
"types": "types/index.d.ts",
"bin": "./bin/cli.mjs",
"exports": {
".": {
"import": "./src/index.mjs",
"types": "./types/index.d.ts"
},
"./package.json": "./package.json"
},
"scripts": {
"test": "./bin/cli.mjs -w test"
},
"files": [
"bin/",
"src/",
"templates/",
"types/",
"vendor/"
],
"engines": {
"node": ">=18.14.1"
},
"repository": {
"type": "git",
"url": "https//github.com/esm-dev/esm.sh"
},
"license": "MIT"
}
File renamed without changes.
52 changes: 18 additions & 34 deletions packages/esm-hot/src/index.mjs → packages/esm-cli/src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
import { openFile } from "./fs.mjs";

const enc = new TextEncoder();
const regexpPluginNaming = /^[a-zA-Z0-9][\w\.\-]*(@\d+\.\d+\.\d+)?$/;

/**
* serves a hot app.
* Creates a fetch handler for serving hot applications.
* @param {import("../types").ServeOptions} options
* @returns {(req: Request) => Promise<Response>}
*/
export const serveHot = (options) => {
if (options.plugins) {
options.plugins = options.plugins.filter((name) =>
regexpPluginNaming.test(name)
);
}
const { spa, watch, plugins, cwd = "." } = options;
const { root = ".", fallback = "index.html", watch } = options;
const fsWatchHandlers = new Set();
if (watch) {
import("node:fs").then(({ watch }) => {
watch(
cwd,
root,
{ recursive: true },
(event, filename) => {
fsWatchHandlers.forEach((handler) =>
handler(event === "change" ? "modify" : event, "/" + filename)
);
if (!/(^|\/)(\.|node_modules\/)/.test(filename) && !filename.endsWith(".log")) {
fsWatchHandlers.forEach((handler) =>
handler(event === "change" ? "modify" : event, "/" + filename)
);
}
},
);
console.log(`Watching files changed...`);
});
}
if (plugins?.length) {
console.log(`Using plugins: ${plugins.join(", ")}`);
}
return async (req) => {
const url = new URL(req.url);
const pathname = decodeURIComponent(url.pathname);
Expand Down Expand Up @@ -62,12 +55,9 @@ export const serveHot = (options) => {
},
);
}
let file = pathname.includes(".") ? await openFile(cwd + pathname) : null;
let file = pathname.includes(".") ? await openFile(root + pathname) : null;
if (!file && pathname === "/sw.js") {
const hotUrl = new URL("https://esm.sh/v135/hot");
if (plugins?.length) {
hotUrl.searchParams.set("plugins", plugins);
}
return new Response(`import hot from "${hotUrl.href}";hot.listen();`, {
headers: {
"content-type": "application/javascript; charset=utf-8",
Expand All @@ -76,19 +66,15 @@ export const serveHot = (options) => {
});
}
if (!file) {
if (spa) {
const index = "index.html";
if (typeof spa === "string" && spa.endsWith(".html")) {
index = spa;
} else if (spa.index && spa.index.endsWith(".html")) {
index = spa.index;
}
file = await openFile(cwd + "/" + index);
} else {
file = await openFile(cwd + pathname + ".html");
if (!file) {
file = await openFile(cwd + pathname + "/index.html");
}
const list = [
pathname + ".html",
pathname + "/index.html",
"/404.html",
"/" + fallback,
];
for (const filename of list) {
file = await openFile(root + filename);
if (file) break;
}
}
if (file) {
Expand Down Expand Up @@ -123,5 +109,3 @@ export const serveHot = (options) => {
return new Response("Not Found", { status: 404 });
};
};

export default serveHot;
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
<script type="importmap">
{
"imports": {
"@jsxImportSource": "https://esm.sh/[email protected]"
"@jsxImportSource": "https://esm.sh/[email protected]",
"react": "https://esm.sh/[email protected]",
}
}
</script>
</head>

<body>
<react-root src="./App.tsx"></react-root>
<react-root src="./App.jsx"></react-root>
<script type="module">
import hot from "https://esm.sh/v135/hot?plugins=react-root"
hot.fire()
Expand Down
3 changes: 3 additions & 0 deletions packages/esm-cli/templates/react/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import hot from "https://esm.sh/v135/hot?plugins=tsx,md"

hot.listen()
11 changes: 11 additions & 0 deletions packages/esm-cli/templates/svelte/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
let name = 'Svelte';
</script>

<h1>Hello {name}!</h1>

<style>
h1 {
color: #ff4000;
}
</style>
25 changes: 25 additions & 0 deletions packages/esm-cli/templates/svelte/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔥</title>
<script type="importmap">
{
"imports": {
"svelte": "https://esm.sh/[email protected]",
}
}
</script>
</head>

<body>
<svelte-root src="./App.svelte"></svelte-root>
<script type="module">
import hot from "https://esm.sh/v135/hot?plugins=svelte-root"
hot.fire()
</script>
</body>

</html>
3 changes: 3 additions & 0 deletions packages/esm-cli/templates/svelte/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import hot from "https://esm.sh/v135/hot?plugins=svelte"

hot.listen()
16 changes: 16 additions & 0 deletions packages/esm-cli/templates/vue/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup>
import { ref } from 'vue'
const msg = ref('Hello Vue!')
</script>

<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>

<style scoped>
h1 {
color: #42b883;
}
</style>
25 changes: 25 additions & 0 deletions packages/esm-cli/templates/vue/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔥</title>
<script type="importmap">
{
"imports": {
"vue": "https://esm.sh/[email protected]",
}
}
</script>
</head>

<body>
<vue-root src="./App.vue"></vue-root>
<script type="module">
import hot from "https://esm.sh/v135/hot?plugins=vue-root"
hot.fire()
</script>
</body>

</html>
3 changes: 3 additions & 0 deletions packages/esm-cli/templates/vue/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import hot from "https://esm.sh/v135/hot?plugins=vue"

hot.listen()
22 changes: 22 additions & 0 deletions packages/esm-cli/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface FsFile {
size: number;
lastModified: number | null;
contentType: string;
body: ReadableStream<Uint8Array>;
close: () => Promise<void>;
}

/** The options for `serveHost` */
export interface ServeOptions {
/** The root path, default to current working directory. */
root?: string;
/** The fallback route, default is `index.html`. */
fallback?: `${string}.html`;
/** Wtaching file changes for HMR, default is `false` */
watch?: boolean;
}

/** Creates a fetch handler for serving hot applications. */
export function serveHost(
options?: ServeOptions,
): (req: Request) => Promise<Response>;
Loading

0 comments on commit 48bfa36

Please sign in to comment.