pax_global_header00006660000000000000000000000064152124522010014504gustar00rootroot0000000000000052 comment=57afed5bb52599934596c212492abfa910041682 esmoduleserve/000077500000000000000000000000001521245220100137325ustar00rootroot00000000000000esmoduleserve/.gitignore000066400000000000000000000000261521245220100157200ustar00rootroot00000000000000/node_modules .tern-* esmoduleserve/LICENSE000066400000000000000000000021011521245220100147310ustar00rootroot00000000000000Copyright (C) 2020 by Marijn Haverbeke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. esmoduleserve/README.md000066400000000000000000000050231521245220100152110ustar00rootroot00000000000000# esmoduleserve This is a shim HTTP server for directly running ES6 modules with non-precise import targets in your browser (without a bundling step). It acts as a regular file server for a given directory, but exposes an extra top-level path, `/_m/`, to serve rewritten modules relative to that directory. Any file requested through this path will have its imports (and re-exports) rewritten to point at precise resolved scripts paths, referenced through `/_m/`. Resolution is done via the [node algorithm](https://www.npmjs.com/package/resolve), but letting `"module"` or `"jsnext"` fields in package.json take precedence over `"main"`. If some of the dependencies you load through this don't provide ES module files, you are likely to find an error about a missing import on your devtools console. You can specify module files from parent directories of the served directory using `/__` to stand in for `/..` in a `/_m/` path. By default, to avoid accidentally serving things you don't want to expose, this is only allowed one parent directory deep. ## Usage You run the server for a given directory... esmoduleserve demo/ --port 8080 It will start up an HTTP server on the given port, serving the content of the `demo` directory statically. If there's a module called `demo.js` in this directory, you can load it in an HTML file with a script tag like this: The options recognized by the command-line server are: * **`--port`** to specify a TCP port to listen on. Defaults to 8080. * **`--host`** to specify a hostname to listen on. Defaults to `"localhost"`. * **`--depth`** to specify how many parent directories should be accessible. Defaults to 1. * **`--prefix`** to specify an alternative URL prefix for module script URLs. Defaults to `"_m"`. The `moduleserver.js` file exports this functionality as HTTP middleware. Usage looks something like: const {ModuleServer} = require("esmoduleserve/moduleserver") const moduleServer = new ModuleServer({root: "/some/path", maxDepth: 2, prefix: "_m"}) // In a server function if (moduleServer.handleRequest(req, resp)) return The `handleRequest` method handles only requests whose path starts with the prefix. It returns true for such requests. ## Source This code is open-source under an MIT license. If you want to contribute, create pull requests [on the online repository](https://code.haverbeke.berlin/marijn/esmoduleserve). esmoduleserve/index.js000077500000000000000000000024071521245220100154050ustar00rootroot00000000000000#!/usr/bin/env node const ModuleServer = require("./moduleserver") const path = require("path") let host = "localhost", port = 8080, dir = ".", prefix = null, maxDepth = 1 function usage() { console.log("Usage: esmoduleserve [--port port] [--host host] [--depth n] [--prefix prefix] [dir]") process.exit(1) } for (var i = 2; i < process.argv.length; i++) { let arg = process.argv[i], next = process.argv[i + 1] if (arg == "--port" && next) { port = +next; i++ } else if (arg == "--host" && next) { host = next; i++ } else if (arg == "--prefix" && next) { prefix = next; i++ } else if (arg == "--depth" && /^\d+$/.test(next)) { maxDepth = +next; i++ } else if (dir == "." && arg[0] != "-") dir = arg else usage() } // The root directory being served. const root = path.resolve(dir) const static = require("serve-static")(root) const moduleServer = new ModuleServer({root, maxDepth, prefix}).handleRequest // Create the server that listens to HTTP requests // and returns module contents. require("http").createServer((req, resp) => { if (moduleServer(req, resp)) return static(req, resp, function next(err) { resp.statusCode = 404 resp.end('Not found') }) }).listen(port, host) console.log("Module server listening on http://" + host + ":" + port) esmoduleserve/moduleserver.js000066400000000000000000000123361521245220100170110ustar00rootroot00000000000000const pth = require("path"), fs = require("fs") const resolve = require("import-meta-resolve") const {parse: parseURL} = require("url") const crypto = require("crypto") const acorn = require("acorn"), walk = require("acorn-walk") class Cached { constructor(content, mimetype) { this.content = content this.headers = { "content-type": mimetype + "; charset=utf-8", "etag": '"' + hash(content) + '"' } } } class ModuleServer { constructor(options) { this.root = unwin(options.root) this.maxDepth = options.maxDepth == null ? 1 : options.maxDepth let base = this.root for (let i = 0; i < this.maxDepth; i++) base = pth.dirname(base) if (!base.endsWith(pth.sep)) base += pth.sep this.basePath = base this.prefix = options.prefix || "_m" this.prefixTest = new RegExp(`^/${this.prefix}/(.*)`) this.transform = options.transform || ((_, c) => c) if (this.root.charAt(this.root.length - 1) != "/") this.root += "/" // Maps from paths (relative to root dir) to cache entries this.cache = Object.create(null) this.handleRequest = this.handleRequest.bind(this) } handleRequest(req, resp) { let url = parseURL(req.url) let handle = this.prefixTest.exec(url.pathname) if (!handle) return false let send = (status, text, headers) => { let hds = {"access-control-allow-origin": "*", "x-request-url": req.url} if (!headers || typeof headers == "string") hds["content-type"] = headers || "text/plain" else Object.assign(hds, headers) resp.writeHead(status, hds) resp.end(text) } // Modules paths in URLs represent "up one directory" as "__". // Convert them to ".." for filesystem path resolution. let path = undash(handle[1]) let cached = this.cache[path] if (!cached) { if (pth.isAbsolute(path) || countParentRefs(path) > this.maxDepth) { send(403, "Access denied"); return true } let joined = unwin(pth.join(this.root, path)) if (!joined.startsWith(this.basePath)) { send(403, "Access denied"); return true } let fullPath = unwin(pth.resolve(joined)), code try { code = fs.readFileSync(fullPath, "utf8") } catch { send(404, "Not found"); return true } if (/\.map$/.test(fullPath)) { cached = this.cache[path] = new Cached(code, "application/json") } else { let {code: resolvedCode, error} = this.resolveImports(fullPath, this.transform(fullPath, code)) if (error) { send(500, error); return true } cached = this.cache[path] = new Cached(resolvedCode, "application/javascript") } // Drop cache entry when the file changes. let watching = fs.watch(fullPath, () => { watching.close() this.cache[path] = null }) } let noneMatch = req.headers["if-none-match"] if (noneMatch && noneMatch.indexOf(cached.headers.etag) > -1) { send(304, null); return true } send(200, cached.content, cached.headers) return true } // Resolve a module path to a relative filepath where // the module's file exists. resolveModule(srcPath, path) { let resolved try { resolved = resolveMod(path, srcPath) } catch(e) { return {error: e.toString()} } // Builtin modules resolve to strings like "fs". Try again with // slash which makes it possible to locally install an equivalent. if (resolved.indexOf("/") == -1) { try { resolved = resolveMod(path + "/", srcPath) } catch(e) { return {error: e.toString()} } } return {path: "/" + this.prefix + "/" + unwin(pth.relative(this.root, resolved))} } resolveImports(srcPath, code) { let patches = [], ast try { ast = acorn.parse(code, {sourceType: "module", ecmaVersion: "latest"}) } catch(error) { return {error: error.toString()} } let patchSrc = (node) => { if (!node.source) return let orig = (0, eval)(code.slice(node.source.start, node.source.end)) let {error, path} = this.resolveModule(srcPath, orig) if (error) return {error} patches.push({from: node.source.start, to: node.source.end, text: JSON.stringify(dash(path))}) } walk.simple(ast, { ExportNamedDeclaration: patchSrc, ImportDeclaration: patchSrc, ImportExpression: node => { if (node.source.type == "Literal") { let {error, path} = this.resolveModule(srcPath, node.source.value) if (!error) patches.push({from: node.source.start, to: node.source.end, text: JSON.stringify(dash(path))}) } } }) for (let patch of patches.sort((a, b) => b.from - a.from)) code = code.slice(0, patch.from) + patch.text + code.slice(patch.to) return {code} } } module.exports = ModuleServer function dash(path) { return path.replace(/(^|\/)\.\.(?=$|\/)/g, "$1__") } function undash(path) { return path.replace(/(^|\/)__(?=$|\/)/g, "$1..") } const unwin = pth.sep == "\\" ? s => s.replace(/\\/g, "/") : s => s function resolveMod(path, base) { let url = resolve.resolve(path, "file://" + base) return fs.realpathSync(url.slice(7)) } function hash(str) { let sum = crypto.createHash("sha1") sum.update(str) return sum.digest("hex") } function countParentRefs(path) { let re = /(^|\/)\.\.(?=\/|$)/g, count = 0 while (re.exec(path)) count++ return count } esmoduleserve/package.json000066400000000000000000000012651521245220100162240ustar00rootroot00000000000000{ "name": "esmoduleserve", "version": "0.3.3", "description": "Serve ES modules over HTTP, rewriting imports", "main": "index.js", "bin": { "esmoduleserve": "./index.js" }, "repository": { "type": "git", "url": "git+https://code.haverbeke.berlin/marijn/esmoduleserve.git" }, "homepage": "https://code.haverbeke.berlin/marijn/esmoduleserve", "keywords": [ "module", "es module", "http", "development", "dev server" ], "author": "Marijn Haverbeke ", "license": "MIT", "dependencies": { "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "import-meta-resolve": "^4.2.0", "serve-static": "^1.14.1" } }