UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

163 lines (135 loc) 4.81 kB
// @ts-check import fs from 'node:fs' import path from 'node:path' import http, { OutgoingMessage } from 'node:http' import https from 'node:https' import { readFile } from 'node:fs/promises' import { contentTypes } from '../content-types.js' import { FileChangeStream } from '../autoreload/streams.js' import { fileURLToPath, pathToFileURL } from 'node:url' const htmlPath = new URL('./public/index.html', import.meta.url) const htmlFileContents = await readFile(htmlPath) const importMap = { "@applicvision/": "/node_modules/@applicvision/", "node:assert/strict": "/public/browser-assert.js" } try { const packageData = await readFile('./package.json') const { name, exports } = JSON.parse(packageData.toString()) // replace self imports for (const [exportName, exportPath] of Object.entries(exports)) { importMap[path.posix.join(name, exportName)] = path.posix.join('/hostproject', typeof exportPath == 'string' ? exportPath : exportPath.import ?? exportPath.default ) } } catch (error) { console.warn('Could not find package.json') } let htmlString = '' /** @param {string[]} mapImports */ function buildHtmlString(mapImports) { mapImports .map(mapImport => mapImport.split(':')) .forEach(([importName, mappedPath]) => { importMap[importName] = mappedPath }) htmlString = htmlFileContents.toString().replace( /(<script type="importmap">).*?(<\/script>)/s, `$1\n${JSON.stringify({ imports: importMap }, null, 2)}\n$2`) } const routes = { testfiles: '/testfiles', nodeModules: '/node_modules/', hostProject: '/hostproject/', public: '/public/', filechangestream: '/filechangestream', } /** * @param {string[]} paths * @param {Set<OutgoingMessage>} clientsToNotify */ async function watchPaths(paths, clientsToNotify) { for await (const change of new FileChangeStream(paths, [], { objectMode: true })) { clientsToNotify.forEach(stream => stream.write(`event: filechange\ndata: ${change.file}\n\n`)) } } /** * @param {URL[]} testFiles * @param {string[]} watchedPaths */ export function createServer(testFiles, watchedPaths, certPath = process.env.JS_TOOLBOX_CERT) { /** @type {Set<OutgoingMessage>} */ const attachedClients = new Set() const cwdFileUrl = pathToFileURL(process.cwd()) cwdFileUrl.pathname += '/' const testFileUrls = testFiles .filter(url => url.pathname.startsWith(cwdFileUrl.pathname)) .map(url => url.pathname.slice((cwdFileUrl.pathname).length)) const server = certPath ? https.createServer({ key: fs.readFileSync(path.resolve(certPath, 'key.pem')), cert: fs.readFileSync(path.resolve(certPath, 'cert.pem')) }) : http.createServer() if (watchedPaths) { const paths = [].concat(watchedPaths) watchPaths(paths, attachedClients) } server.on('request', async (request, response) => { const requestedPath = request.url if (!requestedPath) { return response.writeHead(400).end() } if (requestedPath.startsWith(routes.public)) { const fileUrl = new URL(`.${requestedPath}`, import.meta.url) try { response.writeHead(200, contentTypes[path.extname(fileURLToPath(fileUrl))]) fs.createReadStream(fileUrl).pipe(response) } catch (e) { response.writeHead(404).end() } } else if (requestedPath.startsWith(routes.nodeModules)) { try { const requestedModule = requestedPath.slice(routes.nodeModules.length) const resolvedPath = import.meta.resolve(requestedModule) const fileURL = new URL(resolvedPath) response.writeHead(200, { 'content-type': 'application/javascript' }) fs.createReadStream(fileURL).pipe(response) } catch (error) { response.writeHead(404, 'Module not found').end() } } else if (requestedPath == routes.testfiles) { response.setHeader('content-type', 'application/json') response.end(JSON.stringify({ files: testFileUrls, watchedPaths })) } else if (requestedPath.startsWith(routes.hostProject)) { const fileUrl = new URL(requestedPath.slice(routes.hostProject.length), cwdFileUrl) response.writeHead(200, contentTypes[path.extname(fileURLToPath(fileUrl))]) fs.createReadStream(fileUrl).pipe(response) } else if (requestedPath == routes.filechangestream) { if (!watchedPaths) { return response.end() } response.writeHead(200, { 'content-type': 'text/event-stream' }) attachedClients.add(response) response.write(`event: welcome\ndata:${Date.now()}\n\n`) response.on('close', () => { attachedClients.delete(response) }) } else { response.writeHead(200, contentTypes['.html']) response.end(htmlString) } }) return { start(port, mapImports) { buildHtmlString(mapImports) return new Promise(resolve => server.listen(port, () => resolve(`${certPath ? 'https' : 'http'}://localhost:${port}`))) }, stop() { server.close() } } }