From 7c5792328fedd8ad6c9aa980c5f05294b494fe5d Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 20 Jul 2024 13:15:59 +0200 Subject: [PATCH 01/29] feat(#103): live reload --- src/routes/viewer.ts | 21 +++++++++++++++++---- src/utils/path.ts | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index efbab9f0..92fc6c3e 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -6,7 +6,7 @@ import { Request, Response, Router } from 'express'; import { messageClientsAt } from '../app.js'; import config from '../parser/config.js'; -import { absPath, pcomponents, pmime, preferredPath } from '../utils/path.js'; +import { absPath, isTextFile, pcomponents, preferredPath } from '../utils/path.js'; import { renderDirectory, renderTextFile } from '../parser/parser.js'; export const router = Router(); @@ -43,9 +43,9 @@ router.get(/.*/, async (req: Request, res: Response) => { body = renderDirectory(path); } else { const data = readFileSync(path); - const type = pmime(path); + const [isPlainText, type] = isTextFile(path); - if (!(type.startsWith('text/') || type === 'application/json')) { + if (!isPlainText) { res.setHeader('Content-Type', type).send(data); return; } @@ -90,10 +90,23 @@ router.get(/.*/, async (req: Request, res: Response) => { `); }); +// POST: +// - `cursor`: scroll to corresponding line in source file +// - `content`: set content for live viewer +// - `reload`: set live content to file content (overwrites `content`) router.post(/.*/, async (req: Request, res: Response) => { const path = res.locals.filepath; - const { content, cursor } = req.body; + const { cursor, reload } = req.body; + let { content } = req.body; + if (reload) { + const [isPlainText] = isTextFile(path); + if (!isPlainText) { + res.status(400).send('Reload is only permitted on plain text files'); + return; + } + content = readFileSync(path).toString(); + } if (content) { const rendered = renderTextFile(content, path); liveContent.set(path, rendered); diff --git a/src/utils/path.ts b/src/utils/path.ts index 38cd7e24..aeb92082 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -35,3 +35,8 @@ export const pathToURL = (path: string, route: string = 'viewer') => { export const preferredPath = (path: string): string => config.preferHomeTilde && path.startsWith(homedir()) ? path.replace(homedir(), '~') : path; + +export const isTextFile = (path: string): [boolean, string] => { + const type = pmime(path); + return [type.startsWith('text/') || type === 'application/json', type]; +}; From 585f7189649c94aa98044dfd891fcca6bda42791 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 16:18:36 +0200 Subject: [PATCH 02/29] feat!(#103): remove port configurability in config file --- docs/customization.md | 19 ++++++++++++++----- src/parser/config.ts | 11 +++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 012ffdf4..ce152db7 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -3,6 +3,8 @@ Vivify offers various configuration options. It aims to have sensible defaults while being built for maximal customizability. +## Configuration file + Vivify will look for an optional config file at `~/.vivify/config.json` and `~/.vivify.json`. This file should contain a JSON object that can have the following optional keys: @@ -19,13 +21,9 @@ following optional keys: A path to a file with globs to ignore in Vivify's directory viewer, or an array of multiple paths to ignore files. The syntax here is the same as in `.gitignore` files. -- **`"port"`**\ - The port Vivify's server should run on; this will be overwritten by - the environment variable `VIV_PORT` (default is 31622) - **`"timeout"`**\ How long the server should wait in milliseconds before shutting down after the - last client disconnected; this will be overwritten by the environment variable - `VIV_TIMEOUT` (default is 10000) + last client disconnected (default is 10000) - **`"pageTitle"`**\ JavaScript code that will be evaluated to determine the viewer's page title. Here, the variable `components` is set to a string array of path components @@ -63,3 +61,14 @@ following optional keys: "includeLevel": [2, 3] } ``` + +## Environment variables + +In addition to these config file entries, the following options can be set +through environment variables. + +- **`VIV_PORT`**\ + The port Vivify's server should run on (default is 31622) +- **`VIV_TIMEOUT`**\ + Same as `"timeout"` from config file above but takes precedence over the + setting in the config file diff --git a/src/parser/config.ts b/src/parser/config.ts index 53197977..54da20f2 100644 --- a/src/parser/config.ts +++ b/src/parser/config.ts @@ -2,6 +2,8 @@ import fs from 'fs'; import { homedir } from 'os'; import path from 'path'; +// NOTE: this does type not directly correspond to the config file: see +// defaultConfig, envConfigs and configFileBlocked type Config = { styles?: string; scripts?: string; @@ -18,6 +20,7 @@ type Config = { /* eslint-enable @typescript-eslint/no-explicit-any */ }; +// fills in values from config file config that are not present const defaultConfig: Config = { port: 31622, mdExtensions: ['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn'], @@ -25,11 +28,15 @@ const defaultConfig: Config = { preferHomeTilde: true, }; +// configs that are overwritten by environment variables const envConfigs: [string, keyof Config][] = [ ['VIV_PORT', 'port'], ['VIV_TIMEOUT', 'timeout'], ]; +// configs that can't be set through the config file +const configFileBlocked: (keyof Config)[] = ['port']; + const configPaths = [ path.join(homedir(), '.vivify', 'config.json'), path.join(homedir(), '.vivify.json'), @@ -63,6 +70,10 @@ const getConfig = (): Config => { // revert to default config if no config found config = config ?? defaultConfig; + for (const key of configFileBlocked) { + delete config[key]; + } + // get styles, scripts and ignore files config.styles = getFileContents(config.styles); config.scripts = getFileContents(config.scripts); From 94f66465753f58b7d642a9b71ed4bb6701926c76 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 16:26:08 +0200 Subject: [PATCH 03/29] refactor(#103): scrollTo client function --- static/client.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/static/client.js b/static/client.js index 0e4e94cb..2cb0b33b 100644 --- a/static/client.js +++ b/static/client.js @@ -1,3 +1,18 @@ +function viv_scrollTo(value) { + let line = parseInt(value); + while (line) { + const targets = document.querySelectorAll(`[data-source-line="${line - 1}"]`); + if (targets.length) { + targets[0].scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + break; + } + line -= 1; + } +} + const ws = new WebSocket(`ws://localhost:${window.VIV_PORT}`); ws.addEventListener('open', () => { @@ -15,18 +30,7 @@ ws.addEventListener('message', (event) => { document.getElementById('body-content').innerHTML = value; break; case 'SCROLL': - let line = parseInt(value); - while (line) { - const targets = document.querySelectorAll(`[data-source-line="${line - 1}"]`); - if (targets.length) { - targets[0].scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); - break; - } - line -= 1; - } + viv_scrollTo(value); break; case 'RELOAD': window.location.reload(); From bc10540789ea15e5dbd94b9b0b33e577ab90e708 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 16:36:39 +0200 Subject: [PATCH 04/29] feat(#103): set initial scroll position by query --- src/routes/viewer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index 92fc6c3e..8a018e7e 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -35,6 +35,7 @@ if (config.preferHomeTilde) { router.get(/.*/, async (req: Request, res: Response) => { const path = res.locals.filepath; + const { cursor } = req.query; let body = liveContent.get(path); if (!body) { @@ -86,6 +87,7 @@ router.get(/.*/, async (req: Request, res: Response) => { ${config.scripts ? `` : ''} + ${cursor !== undefined ? `` : ''} `); }); From 943dffab4f90382c910ee1528f565e5dac6d687e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 16:47:37 +0200 Subject: [PATCH 05/29] feat(#103): remove query params on load --- static/client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/client.js b/static/client.js index 2cb0b33b..b0333d77 100644 --- a/static/client.js +++ b/static/client.js @@ -40,3 +40,7 @@ ws.addEventListener('message', (event) => { break; } }); + +// remove query parameters from URL so that opening the same file will direct to +// the same browser window even if it was initially opened at a given position +history.replaceState(null, '', window.location.origin + window.location.pathname); From 2941364516de9890b962e6a067ecd4553d4f6da0 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 17:12:14 +0200 Subject: [PATCH 06/29] feat(#103): return number of clients on post/delete --- src/app.ts | 2 +- src/routes/viewer.ts | 25 ++++++++++++++++++------- src/sockets.ts | 6 +++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/app.ts b/src/app.ts index 4376f3f4..d5db496d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ app.use('/viewer', viewerRouter); const server = createServer(app); let shutdownTimer: NodeJS.Timeout | null = null; -export const { clientsAt, messageClientsAt } = setupSockets( +export const { clientsAt, messageClients } = setupSockets( server, () => { if (config.timeout > 0) diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index 8a018e7e..101a9bde 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -4,7 +4,7 @@ import { homedir } from 'os'; import { Request, Response, Router } from 'express'; -import { messageClientsAt } from '../app.js'; +import { clientsAt, messageClients } from '../app.js'; import config from '../parser/config.js'; import { absPath, isTextFile, pcomponents, preferredPath } from '../utils/path.js'; import { renderDirectory, renderTextFile } from '../parser/parser.js'; @@ -109,24 +109,35 @@ router.post(/.*/, async (req: Request, res: Response) => { } content = readFileSync(path).toString(); } + const clients = clientsAt(path); if (content) { const rendered = renderTextFile(content, path); liveContent.set(path, rendered); - messageClientsAt(path, `UPDATE: ${rendered}`); + messageClients(clients, `UPDATE: ${rendered}`); } - if (cursor) messageClientsAt(path, `SCROLL: ${cursor}`); + if (cursor) messageClients(clients, `SCROLL: ${cursor}`); - res.end(); + res.send({ clients: clients.length }); }); router.delete(/.*/, async (req: Request, res: Response) => { const path = req.path; + let clientCount = 0; + if (path === '/') { const paths = [...liveContent.keys()]; liveContent.clear(); - paths.forEach((path) => messageClientsAt(path, 'RELOAD: 1')); + clientCount = paths.reduce((count, path) => { + const clients = clientsAt(path); + messageClients(clients, 'RELOAD: 1'); + return count + clients.length; + }, 0); } else { - liveContent.delete(path) && messageClientsAt(path, 'RELOAD: 1'); + const clients = clientsAt(path); + if (liveContent.delete(path)) { + messageClients(clients, 'RELOAD: 1'); + clientCount = clients.length; + } } - res.end(); + res.send({ clients: clientCount }); }); diff --git a/src/sockets.ts b/src/sockets.ts index 3581ab14..fa536351 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -57,8 +57,8 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli wss.on('close', () => clearInterval(interval)); const clientsAt = (p: string) => [...sockets.values()].filter(({ path }) => path == p); - const messageClientsAt = (p: string, message: string) => - clientsAt(p).forEach(({ socket }) => socket.send(message)); + const messageClients = (clients: SocketData[], message: string) => + clients.forEach(({ socket }) => socket.send(message)); - return { clientsAt, messageClientsAt }; + return { clientsAt, messageClients }; } From 6ea498ce8f9e29e80cb6d3dd6449cbc1f83e8b40 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 22 Jul 2024 17:54:11 +0200 Subject: [PATCH 07/29] feat(#103): open at scroll on same tab --- package.json | 1 + src/app.ts | 44 ++---------------------- src/cli.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++ src/routes/viewer.ts | 2 +- viv | 4 +-- yarn.lock | 47 ++++++++++++++++++++++++- 6 files changed, 134 insertions(+), 46 deletions(-) create mode 100644 src/cli.ts diff --git a/package.json b/package.json index 42c3fdf0..ecca2412 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@viz-js/viz": "^3.7.0", "ansi_up": "^6.0.2", + "axios": "^1.7.2", "express": "^4.19.2", "glob": "10.4.5", "highlight.js": "^11.10.0", diff --git a/src/app.ts b/src/app.ts index d5db496d..e7ee5d55 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,16 +1,14 @@ import { createServer, get } from 'http'; -import { resolve as presolve } from 'path'; import express from 'express'; -import open from 'open'; import config from './parser/config.js'; import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; import { setupSockets } from './sockets.js'; -import { pathToURL, preferredPath, urlToPath } from './utils/path.js'; -import { existsSync } from 'fs'; +import { urlToPath } from './utils/path.js'; +import { address, handleArgs } from './cli.js'; const app = express(); app.use(express.json()); @@ -39,44 +37,6 @@ export const { clientsAt, messageClients } = setupSockets( }, ); -const address = `http://localhost:${config.port}`; -const handleArgs = async () => { - try { - const args = process.argv.slice(2); - const options = args.filter((arg) => arg.startsWith('-')); - for (const option of options) { - switch (option) { - case '-v': - case '--version': - console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`); - break; - default: - console.log(`unknown option "${option}"`); - } - } - - const paths = args.filter((arg) => !arg.startsWith('-')); - await Promise.all( - paths.map(async (path) => { - if (!existsSync(path)) { - console.log(`File not found: ${path}`); - return; - } - const target = preferredPath(presolve(path)); - const url = `${address}${pathToURL(target)}`; - await open(url); - }), - ); - } finally { - if (process.env['NODE_ENV'] !== 'development') { - // - viv executable waits for this string and then stops printing - // vivify-server's output and terminates - // - the string itself is not shown to the user - console.log('STARTUP COMPLETE'); - } - } -}; - get(`${address}/health`, async () => { // server is already running await handleArgs(); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..fa80deeb --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,82 @@ +import { existsSync } from 'fs'; +import { resolve as presolve } from 'path'; +import config from './parser/config.js'; +import open from 'open'; +import { pathToURL, preferredPath } from './utils/path.js'; +import axios from 'axios'; + +export const address = `http://localhost:${config.port}`; + +const openTarget = async (path: string, scroll: string | undefined) => { + // scroll position is zero-char seperated because that + // character is not allowed in file paths + if (!existsSync(path)) { + console.log(`File not found: ${path}`); + return; + } + + const resolvedPath = presolve(path); + const absoluteURL = `${address}${pathToURL(resolvedPath)}`; + const preferredURL = `${address}${pathToURL(preferredPath(resolvedPath))}`; + // if scroll position is provided + if (scroll !== undefined) { + // we send scroll request to clients + const { + data: { clients }, + } = await axios.post<{ clients: number }>(absoluteURL, { + cursor: scroll, + }); + // if there were clients, we can just open the plain + // URL/existing tab because it will have scrolled + if (!clients) { + // if not we open a new tab at the scroll position + await open(preferredURL + `?cursor=${scroll}`); + return; + } + } + await open(preferredURL, { newInstance: false }); +}; + +export const handleArgs = async () => { + try { + const args = process.argv.slice(2); + const parsed: { target?: string; scroll?: string } = {}; + const setArg = (arg: keyof typeof parsed, value: string) => { + if (arg in parsed) { + console.log(`Duplicate argument for "${arg}", skipping`); + } else { + parsed[arg] = value; + } + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (!arg.startsWith('-')) { + setArg('target', arg); + continue; + } + switch (arg) { + case '-v': + case '--version': + console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`); + break; + case '-s': + case '--scroll': + setArg('scroll', args[++i]); + break; + default: + console.log(`Unknown option "${arg}"`); + } + } + if (parsed.target) { + await openTarget(parsed.target, parsed.scroll); + } + } finally { + if (process.env['NODE_ENV'] !== 'development') { + // - viv executable waits for this string and then stops printing + // vivify-server's output and terminates + // - the string itself is not shown to the user + console.log('STARTUP COMPLETE'); + } + } +}; diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index 101a9bde..ebe86537 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -26,7 +26,7 @@ const pageTitle = (path: string) => { if (config.preferHomeTilde) { router.use((req, res, next) => { if (req.method === 'GET' && req.path.startsWith(homedir())) { - res.redirect(req.baseUrl + req.path.replace(homedir(), '/~')); + res.redirect(req.originalUrl.replace(homedir(), '/~')); } else { next(); } diff --git a/viv b/viv index 30f6514c..683f9ba3 100755 --- a/viv +++ b/viv @@ -1,8 +1,8 @@ #!/bin/sh print_usage() { - echo "viv [--help] files/directories" - echo "View files/directories in browser and lazily start vivify-server" + echo "viv [--help] file/directory" + echo "View file/directory in browser and lazily start vivify-server" } if [ "$#" -lt 1 -o "$1" = "-h" -o "$1" = "--help" ]; then diff --git a/yarn.lock b/yarn.lock index 3ab44ccd..cd56f9c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -700,6 +700,20 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -856,6 +870,13 @@ colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" @@ -961,6 +982,11 @@ define-lazy-prop@^3.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -1331,6 +1357,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + foreground-child@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" @@ -1339,6 +1370,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -1865,7 +1905,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -2130,6 +2170,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pstree.remy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" From 3b38214618d406857215c04487e6ccb3cd6b3c0d Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 23 Jul 2024 19:58:30 +0200 Subject: [PATCH 08/29] feat(#103): remove client on socket close --- src/sockets.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/sockets.ts b/src/sockets.ts index fa536351..230b8cfb 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -14,6 +14,14 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const wss = new WebSocketServer({ server }); const sockets = new Map(); + const terminateSocket = (id: string) => { + const socket = sockets.get(id); + if (!socket) return; + socket.socket.terminate(); + sockets.delete(id); + if (!sockets.size) onNoClients(); + }; + wss.on('connection', (socket) => { if (sockets.size === 0) onFirstClient(); const id = uuidv4(); @@ -39,18 +47,20 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli break; } }); + + socket.on('close', () => { + terminateSocket(id); + }); }); const interval = setInterval(() => { wss.clients.forEach((ws) => ws.ping()); - for (const [id, { socket, alive }] of sockets) { + for (const [id, { alive }] of sockets) { if (alive) { sockets.get(id)!.alive = false; continue; } - socket.terminate(); - sockets.delete(id); - if (!sockets.size) onNoClients(); + terminateSocket(id); } }, 1000); From c76cd3423f609f9b3baa7f97c4dd3ca71adaa31b Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 23 Jul 2024 20:20:19 +0200 Subject: [PATCH 09/29] feat(#103): update help message --- viv | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/viv b/viv index 683f9ba3..b67f033f 100755 --- a/viv +++ b/viv @@ -1,8 +1,14 @@ #!/bin/sh print_usage() { - echo "viv [--help] file/directory" - echo "View file/directory in browser and lazily start vivify-server" + echo "usage: viv [--scroll n] file/directory" + echo "" + echo "View file/directory in your browser with Vivify." + echo "" + echo "options:" + echo " --help show this help message and exit" + echo " --version show version information" + echo " -s n, --scroll n for markdown files, scroll so that content at source line n is visible" } if [ "$#" -lt 1 -o "$1" = "-h" -o "$1" = "--help" ]; then From d421d6bcc9de1c8ea7b004734316d909ecf76b8e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 08:41:07 +0200 Subject: [PATCH 10/29] chore!(#103): remove query approach --- src/routes/viewer.ts | 2 -- static/client.js | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index ebe86537..f0e5c262 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -35,7 +35,6 @@ if (config.preferHomeTilde) { router.get(/.*/, async (req: Request, res: Response) => { const path = res.locals.filepath; - const { cursor } = req.query; let body = liveContent.get(path); if (!body) { @@ -87,7 +86,6 @@ router.get(/.*/, async (req: Request, res: Response) => { ${config.scripts ? `` : ''} - ${cursor !== undefined ? `` : ''} `); }); diff --git a/static/client.js b/static/client.js index b0333d77..2cb0b33b 100644 --- a/static/client.js +++ b/static/client.js @@ -40,7 +40,3 @@ ws.addEventListener('message', (event) => { break; } }); - -// remove query parameters from URL so that opening the same file will direct to -// the same browser window even if it was initially opened at a given position -history.replaceState(null, '', window.location.origin + window.location.pathname); From 4e4ec40e3017b79042f1830447e589d41e6d1e1a Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 08:50:49 +0200 Subject: [PATCH 11/29] feat(#103): queue messages --- src/app.ts | 2 +- src/sockets.ts | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index e7ee5d55..2a157793 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,7 +23,7 @@ app.use('/viewer', viewerRouter); const server = createServer(app); let shutdownTimer: NodeJS.Timeout | null = null; -export const { clientsAt, messageClients } = setupSockets( +export const { clientsAt, messageClients, queueMessage } = setupSockets( server, () => { if (config.timeout > 0) diff --git a/src/sockets.ts b/src/sockets.ts index 230b8cfb..28945820 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -13,6 +13,8 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const wss = new WebSocketServer({ server }); const sockets = new Map(); + // queue of initial messages to be sent to new clients + const messageQueue = new Map(); const terminateSocket = (id: string) => { const socket = sockets.get(id); @@ -44,6 +46,11 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli switch (key) { case 'PATH': sockets.get(id)!.path = value; + const messages = messageQueue.get(value); + if (messages) { + messageQueue.delete(value); + messages.forEach((msg) => socket.send(msg)); + } break; } }); @@ -69,6 +76,14 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const clientsAt = (p: string) => [...sockets.values()].filter(({ path }) => path == p); const messageClients = (clients: SocketData[], message: string) => clients.forEach(({ socket }) => socket.send(message)); + const queueMessage = (path: string, message: string) => { + const messages = messageQueue.get(path); + if (messages) { + messages.push(message); + } else { + messageQueue.set(path, [message]); + } + }; - return { clientsAt, messageClients }; + return { clientsAt, messageClients, queueMessage }; } From ca23da93e29d12db607f1346b5de5f6e8aaddcda Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 09:07:24 +0200 Subject: [PATCH 12/29] feat(#103): scroll with message queuing --- src/app.ts | 2 ++ src/cli.ts | 23 +++++------------------ src/routes/queue.ts | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 src/routes/queue.ts diff --git a/src/app.ts b/src/app.ts index 2a157793..f6307046 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import config from './parser/config.js'; import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; +import { router as queueRouter } from './routes/queue.js'; import { setupSockets } from './sockets.js'; import { urlToPath } from './utils/path.js'; import { address, handleArgs } from './cli.js'; @@ -19,6 +20,7 @@ app.use((req, res, next) => { app.use('/static', staticRouter); app.use('/health', healthRouter); app.use('/viewer', viewerRouter); +app.use('/queue', queueRouter); const server = createServer(app); diff --git a/src/cli.ts b/src/cli.ts index fa80deeb..e47a6c2d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,33 +8,20 @@ import axios from 'axios'; export const address = `http://localhost:${config.port}`; const openTarget = async (path: string, scroll: string | undefined) => { - // scroll position is zero-char seperated because that - // character is not allowed in file paths if (!existsSync(path)) { console.log(`File not found: ${path}`); return; } const resolvedPath = presolve(path); - const absoluteURL = `${address}${pathToURL(resolvedPath)}`; - const preferredURL = `${address}${pathToURL(preferredPath(resolvedPath))}`; - // if scroll position is provided if (scroll !== undefined) { - // we send scroll request to clients - const { - data: { clients }, - } = await axios.post<{ clients: number }>(absoluteURL, { - cursor: scroll, + await axios.post(`${address}/queue`, { + path: resolvedPath, + command: 'SCROLL', + value: scroll, }); - // if there were clients, we can just open the plain - // URL/existing tab because it will have scrolled - if (!clients) { - // if not we open a new tab at the scroll position - await open(preferredURL + `?cursor=${scroll}`); - return; - } } - await open(preferredURL, { newInstance: false }); + await open(`${address}${pathToURL(preferredPath(resolvedPath))}`); }; export const handleArgs = async () => { diff --git a/src/routes/queue.ts b/src/routes/queue.ts new file mode 100644 index 00000000..9d9b228d --- /dev/null +++ b/src/routes/queue.ts @@ -0,0 +1,16 @@ +import { Request, Response, Router } from 'express'; +import { queueMessage } from '../app.js'; + +export const router = Router(); + +router.post('/', async (req: Request, res: Response) => { + const { path, command, value } = req.body; + + if (!path || !command || !value) { + res.status(400).send('Bad request.'); + return; + } + + queueMessage(path, `${command}: ${value}`); + res.end(); +}); From 0daea67054f2b907e69e0d41b4fbbd500fb202ea Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 09:12:06 +0200 Subject: [PATCH 13/29] chore(#103): comment & underscore prefix --- src/app.ts | 2 +- src/cli.ts | 2 +- src/routes/queue.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index f6307046..dd0df91c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,7 +20,7 @@ app.use((req, res, next) => { app.use('/static', staticRouter); app.use('/health', healthRouter); app.use('/viewer', viewerRouter); -app.use('/queue', queueRouter); +app.use('/_queue', queueRouter); const server = createServer(app); diff --git a/src/cli.ts b/src/cli.ts index e47a6c2d..78eb3779 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ const openTarget = async (path: string, scroll: string | undefined) => { const resolvedPath = presolve(path); if (scroll !== undefined) { - await axios.post(`${address}/queue`, { + await axios.post(`${address}/_queue`, { path: resolvedPath, command: 'SCROLL', value: scroll, diff --git a/src/routes/queue.ts b/src/routes/queue.ts index 9d9b228d..997f9aea 100644 --- a/src/routes/queue.ts +++ b/src/routes/queue.ts @@ -3,6 +3,7 @@ import { queueMessage } from '../app.js'; export const router = Router(); +// this route should only be used internally between vivify processes router.post('/', async (req: Request, res: Response) => { const { path, command, value } = req.body; From 308090fd089a5be63280ee6017383ae4407ad9a3 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 09:17:57 +0200 Subject: [PATCH 14/29] feat(#103): clear queue --- src/app.ts | 2 +- src/cli.ts | 7 ++++++- src/routes/queue.ts | 13 +++++++++---- src/sockets.ts | 3 ++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index dd0df91c..6096553e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ app.use('/_queue', queueRouter); const server = createServer(app); let shutdownTimer: NodeJS.Timeout | null = null; -export const { clientsAt, messageClients, queueMessage } = setupSockets( +export const { clientsAt, messageClients, queueMessage, deleteQueue } = setupSockets( server, () => { if (config.timeout > 0) diff --git a/src/cli.ts b/src/cli.ts index 78eb3779..e9616c44 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,7 +21,12 @@ const openTarget = async (path: string, scroll: string | undefined) => { value: scroll, }); } - await open(`${address}${pathToURL(preferredPath(resolvedPath))}`); + try { + await open(`${address}${pathToURL(preferredPath(resolvedPath))}`); + } catch { + // clear query if open failed + await axios.post(`${address}/_queue`, { path: resolvedPath }); + } }; export const handleArgs = async () => { diff --git a/src/routes/queue.ts b/src/routes/queue.ts index 997f9aea..d8c5c967 100644 --- a/src/routes/queue.ts +++ b/src/routes/queue.ts @@ -1,17 +1,22 @@ import { Request, Response, Router } from 'express'; -import { queueMessage } from '../app.js'; +import { deleteQueue, queueMessage } from '../app.js'; +// this route should only be used internally between vivify processes export const router = Router(); -// this route should only be used internally between vivify processes router.post('/', async (req: Request, res: Response) => { const { path, command, value } = req.body; - if (!path || !command || !value) { + if (!path) { res.status(400).send('Bad request.'); return; } - queueMessage(path, `${command}: ${value}`); + if (!command) { + deleteQueue(path); + } else { + queueMessage(path, `${command}: ${value}`); + } + res.end(); }); diff --git a/src/sockets.ts b/src/sockets.ts index 28945820..82405a0e 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -84,6 +84,7 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli messageQueue.set(path, [message]); } }; + const deleteQueue = (path: string) => messageQueue.delete(path); - return { clientsAt, messageClients, queueMessage }; + return { clientsAt, messageClients, queueMessage, deleteQueue }; } From 5d1553f1f45bd0ae750d39e89fdf58a7dd0954b3 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 27 Jul 2024 09:50:44 +0200 Subject: [PATCH 15/29] refactor(#103): handle queue+open server-side --- src/app.ts | 6 +++--- src/cli.ts | 20 +++++--------------- src/routes/_open.ts | 35 +++++++++++++++++++++++++++++++++++ src/routes/queue.ts | 22 ---------------------- src/sockets.ts | 24 +++++++++--------------- 5 files changed, 52 insertions(+), 55 deletions(-) create mode 100644 src/routes/_open.ts delete mode 100644 src/routes/queue.ts diff --git a/src/app.ts b/src/app.ts index 6096553e..6920cf8f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,7 @@ import config from './parser/config.js'; import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; -import { router as queueRouter } from './routes/queue.js'; +import { router as openRouter } from './routes/_open.js'; import { setupSockets } from './sockets.js'; import { urlToPath } from './utils/path.js'; import { address, handleArgs } from './cli.js'; @@ -20,12 +20,12 @@ app.use((req, res, next) => { app.use('/static', staticRouter); app.use('/health', healthRouter); app.use('/viewer', viewerRouter); -app.use('/_queue', queueRouter); +app.use('/_open', openRouter); const server = createServer(app); let shutdownTimer: NodeJS.Timeout | null = null; -export const { clientsAt, messageClients, queueMessage, deleteQueue } = setupSockets( +export const { clientsAt, messageClients, queueMessage, deleteQueuedMessage } = setupSockets( server, () => { if (config.timeout > 0) diff --git a/src/cli.ts b/src/cli.ts index e9616c44..df6892c3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,6 @@ import { existsSync } from 'fs'; import { resolve as presolve } from 'path'; import config from './parser/config.js'; -import open from 'open'; -import { pathToURL, preferredPath } from './utils/path.js'; import axios from 'axios'; export const address = `http://localhost:${config.port}`; @@ -14,19 +12,11 @@ const openTarget = async (path: string, scroll: string | undefined) => { } const resolvedPath = presolve(path); - if (scroll !== undefined) { - await axios.post(`${address}/_queue`, { - path: resolvedPath, - command: 'SCROLL', - value: scroll, - }); - } - try { - await open(`${address}${pathToURL(preferredPath(resolvedPath))}`); - } catch { - // clear query if open failed - await axios.post(`${address}/_queue`, { path: resolvedPath }); - } + await axios.post(`${address}/_open`, { + path: resolvedPath, + command: scroll !== undefined ? 'SCROLL' : undefined, + value: scroll, + }); }; export const handleArgs = async () => { diff --git a/src/routes/_open.ts b/src/routes/_open.ts new file mode 100644 index 00000000..35a7a60f --- /dev/null +++ b/src/routes/_open.ts @@ -0,0 +1,35 @@ +import { Request, Response, Router } from 'express'; +import { deleteQueuedMessage, queueMessage } from '../app.js'; +import { address } from '../cli.js'; +import { pathToURL, preferredPath } from '../utils/path.js'; +import open from 'open'; + +// this route should only be used internally between vivify processes +export const router = Router(); + +router.post('/', async (req: Request, res: Response) => { + const { path, command, value } = req.body; + + if (!path) { + res.status(400).send('Bad request.'); + return; + } + + // NOTE: if we ever want to properly consider having many clients to one + // server (currently not smart because entire file system would be + // exposed), we will have to protect this critical section between here and + // the websocket of the client connecting in `src/sockets.ts` + if (command) { + queueMessage(path, `${command}: ${value}`); + } else { + deleteQueuedMessage(path); + } + + try { + await open(`${address}${pathToURL(preferredPath(path))}`); + } catch { + deleteQueuedMessage(path); + } + + res.end(); +}); diff --git a/src/routes/queue.ts b/src/routes/queue.ts deleted file mode 100644 index d8c5c967..00000000 --- a/src/routes/queue.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { deleteQueue, queueMessage } from '../app.js'; - -// this route should only be used internally between vivify processes -export const router = Router(); - -router.post('/', async (req: Request, res: Response) => { - const { path, command, value } = req.body; - - if (!path) { - res.status(400).send('Bad request.'); - return; - } - - if (!command) { - deleteQueue(path); - } else { - queueMessage(path, `${command}: ${value}`); - } - - res.end(); -}); diff --git a/src/sockets.ts b/src/sockets.ts index 82405a0e..447d6fe2 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -13,8 +13,8 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const wss = new WebSocketServer({ server }); const sockets = new Map(); - // queue of initial messages to be sent to new clients - const messageQueue = new Map(); + // queue of initial message to be sent to new clients + const messageQueue = new Map(); const terminateSocket = (id: string) => { const socket = sockets.get(id); @@ -46,10 +46,10 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli switch (key) { case 'PATH': sockets.get(id)!.path = value; - const messages = messageQueue.get(value); - if (messages) { + const message = messageQueue.get(value); + if (message) { messageQueue.delete(value); - messages.forEach((msg) => socket.send(msg)); + socket.send(message); } break; } @@ -76,15 +76,9 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const clientsAt = (p: string) => [...sockets.values()].filter(({ path }) => path == p); const messageClients = (clients: SocketData[], message: string) => clients.forEach(({ socket }) => socket.send(message)); - const queueMessage = (path: string, message: string) => { - const messages = messageQueue.get(path); - if (messages) { - messages.push(message); - } else { - messageQueue.set(path, [message]); - } - }; - const deleteQueue = (path: string) => messageQueue.delete(path); - return { clientsAt, messageClients, queueMessage, deleteQueue }; + const queueMessage = (path: string, message: string) => messageQueue.set(path, message); + const deleteQueuedMessage = (path: string) => messageQueue.delete(path); + + return { clientsAt, messageClients, queueMessage, deleteQueuedMessage }; } From efd41c9691631bfc9a8d56486a3898abab655136 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Mon, 29 Jul 2024 18:16:30 +0200 Subject: [PATCH 16/29] feat(#103): add -- for targets starting in dashes --- src/cli.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index df6892c3..15a215de 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -30,10 +30,11 @@ export const handleArgs = async () => { parsed[arg] = value; } }; + let parseOptions = true; for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (!arg.startsWith('-')) { + if (!(arg.startsWith('-') && parseOptions)) { setArg('target', arg); continue; } @@ -46,6 +47,9 @@ export const handleArgs = async () => { case '--scroll': setArg('scroll', args[++i]); break; + case '--': + parseOptions = false; + break; default: console.log(`Unknown option "${arg}"`); } From 1669bbb277aac7837406b5324acd99e8089554f6 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 30 Jul 2024 09:07:37 +0200 Subject: [PATCH 17/29] refactor(#103): --help with heredoc --- viv | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/viv b/viv index b67f033f..60b8a89c 100755 --- a/viv +++ b/viv @@ -1,14 +1,17 @@ #!/bin/sh print_usage() { - echo "usage: viv [--scroll n] file/directory" - echo "" - echo "View file/directory in your browser with Vivify." - echo "" - echo "options:" - echo " --help show this help message and exit" - echo " --version show version information" - echo " -s n, --scroll n for markdown files, scroll so that content at source line n is visible" + cat < Date: Tue, 30 Jul 2024 09:10:37 +0200 Subject: [PATCH 18/29] docs(#103): update --help for :n suffix --- viv | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/viv b/viv index 60b8a89c..365c55aa 100755 --- a/viv +++ b/viv @@ -2,15 +2,13 @@ print_usage() { cat < Date: Tue, 30 Jul 2024 12:35:16 +0200 Subject: [PATCH 19/29] feat(#103): colon-based scrolling --- src/cli.ts | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 15a215de..ef46fb1e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,7 +5,23 @@ import axios from 'axios'; export const address = `http://localhost:${config.port}`; -const openTarget = async (path: string, scroll: string | undefined) => { +const getPathAndLine = (target: string): { path: string | undefined; line: number | undefined } => { + const exp = /^(?(?:.*?)(?\d+))?$/; + const groups = target.match(exp)?.groups; + if (groups === undefined || !('path' in groups)) { + return { path: undefined, line: undefined }; + } + const path = groups['path'].replace('\\:', ':').replace('\\\\', '\\'); + const line = 'line' in groups ? parseInt(groups['line']) : undefined; + return { path, line }; +}; + +const openTarget = async (target: string) => { + const { path, line } = getPathAndLine(target); + if (!path) { + console.log(`Invalid target: ${target}`); + return; + } if (!existsSync(path)) { console.log(`File not found: ${path}`); return; @@ -14,28 +30,21 @@ const openTarget = async (path: string, scroll: string | undefined) => { const resolvedPath = presolve(path); await axios.post(`${address}/_open`, { path: resolvedPath, - command: scroll !== undefined ? 'SCROLL' : undefined, - value: scroll, + command: line !== undefined ? 'SCROLL' : undefined, + value: line, }); }; export const handleArgs = async () => { try { const args = process.argv.slice(2); - const parsed: { target?: string; scroll?: string } = {}; - const setArg = (arg: keyof typeof parsed, value: string) => { - if (arg in parsed) { - console.log(`Duplicate argument for "${arg}", skipping`); - } else { - parsed[arg] = value; - } - }; + const positional: string[] = []; let parseOptions = true; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (!(arg.startsWith('-') && parseOptions)) { - setArg('target', arg); + positional.push(arg); continue; } switch (arg) { @@ -43,10 +52,6 @@ export const handleArgs = async () => { case '--version': console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`); break; - case '-s': - case '--scroll': - setArg('scroll', args[++i]); - break; case '--': parseOptions = false; break; @@ -54,9 +59,7 @@ export const handleArgs = async () => { console.log(`Unknown option "${arg}"`); } } - if (parsed.target) { - await openTarget(parsed.target, parsed.scroll); - } + await Promise.all(positional.map((target) => openTarget(target))); } finally { if (process.env['NODE_ENV'] !== 'development') { // - viv executable waits for this string and then stops printing From 42c9a56b4e673d6abd434c734b1bcc9184ffa90b Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 30 Jul 2024 12:37:20 +0200 Subject: [PATCH 20/29] refactor(#103): tests/rendering dir --- README.md | 6 +++--- docs/CONTRIBUTING.md | 4 ++-- tests/{ => rendering}/images/san-juan-mountains.png | Bin tests/{ => rendering}/images/shiprock.png | Bin tests/{ => rendering}/markdown-additional.md | 0 tests/{ => rendering}/markdown-basic.md | 0 tests/{ => rendering}/markdown-extended.md | 0 tests/{ => rendering}/notebook.ipynb | 0 8 files changed, 5 insertions(+), 5 deletions(-) rename tests/{ => rendering}/images/san-juan-mountains.png (100%) rename tests/{ => rendering}/images/shiprock.png (100%) rename tests/{ => rendering}/markdown-additional.md (100%) rename tests/{ => rendering}/markdown-basic.md (100%) rename tests/{ => rendering}/markdown-extended.md (100%) rename tests/{ => rendering}/notebook.ipynb (100%) diff --git a/README.md b/README.md index d46899f7..f629c12e 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ issue](https://github.com/jannis-baum/vivify/issues/new/choose) or - `` tags, e.g. to style keyboard shortcuts You can find examples for all supported features in the files in the -[`tests/`](tests) directory. In case you are looking at these on GitHub, keep in -mind that GitHub doesn't support some of the features that Vivify supports so -some things may look off. +[`tests/rendering`](tests/rendering) directory. In case you are looking at these +on GitHub, keep in mind that GitHub doesn't support some of the features that +Vivify supports so some things may look off. ### Editor Support diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7dcb8691..7499d979 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -115,8 +115,8 @@ above: ## Testing rendering You can find files to test Vivify's rendering/parsing capabilities in the -[`tests/`](tests/) directory. Please make sure to add to this in case you add -anything new related to this. +[`tests/rendering`](tests/rendering) directory. Please make sure to add to this +in case you add anything new related to this. ## Writing Markdown diff --git a/tests/images/san-juan-mountains.png b/tests/rendering/images/san-juan-mountains.png similarity index 100% rename from tests/images/san-juan-mountains.png rename to tests/rendering/images/san-juan-mountains.png diff --git a/tests/images/shiprock.png b/tests/rendering/images/shiprock.png similarity index 100% rename from tests/images/shiprock.png rename to tests/rendering/images/shiprock.png diff --git a/tests/markdown-additional.md b/tests/rendering/markdown-additional.md similarity index 100% rename from tests/markdown-additional.md rename to tests/rendering/markdown-additional.md diff --git a/tests/markdown-basic.md b/tests/rendering/markdown-basic.md similarity index 100% rename from tests/markdown-basic.md rename to tests/rendering/markdown-basic.md diff --git a/tests/markdown-extended.md b/tests/rendering/markdown-extended.md similarity index 100% rename from tests/markdown-extended.md rename to tests/rendering/markdown-extended.md diff --git a/tests/notebook.ipynb b/tests/rendering/notebook.ipynb similarity index 100% rename from tests/notebook.ipynb rename to tests/rendering/notebook.ipynb From ddceb98f6907b074991fec979d75208e7d3d3a6f Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 30 Jul 2024 13:04:54 +0200 Subject: [PATCH 21/29] feat(#103): test & fixes --- package.json | 3 ++- src/cli.ts | 8 ++++--- tests/unit/.gitignore | 1 + tests/unit/cli.ts | 54 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 3 ++- 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/unit/.gitignore create mode 100644 tests/unit/cli.ts diff --git a/package.json b/package.json index ecca2412..fb463269 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "VIV_TIMEOUT=0 VIV_PORT=3000 NODE_ENV=development nodemon --exec node --loader ts-node/esm src/app.ts", "viv": "VIV_PORT=3000 node --loader ts-node/esm src/app.ts", "lint": "eslint src static", - "lint-markdown": "markdownlint-cli2 --config .github/.markdownlint-cli2.yaml" + "lint-markdown": "markdownlint-cli2 --config .github/.markdownlint-cli2.yaml", + "test": "node --loader ts-node/esm tests/unit/cli.ts" }, "type": "module", "dependencies": { diff --git a/src/cli.ts b/src/cli.ts index ef46fb1e..ae1b8ab1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,14 +5,16 @@ import axios from 'axios'; export const address = `http://localhost:${config.port}`; -const getPathAndLine = (target: string): { path: string | undefined; line: number | undefined } => { +export const getPathAndLine = ( + target: string, +): { path: string | undefined; line: number | undefined } => { const exp = /^(?(?:.*?)(?\d+))?$/; const groups = target.match(exp)?.groups; - if (groups === undefined || !('path' in groups)) { + if (groups === undefined || !groups['path']) { return { path: undefined, line: undefined }; } const path = groups['path'].replace('\\:', ':').replace('\\\\', '\\'); - const line = 'line' in groups ? parseInt(groups['line']) : undefined; + const line = groups['line'] ? parseInt(groups['line']) : undefined; return { path, line }; }; diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 00000000..a6c7c285 --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/tests/unit/cli.ts b/tests/unit/cli.ts new file mode 100644 index 00000000..7e3e342f --- /dev/null +++ b/tests/unit/cli.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert'; +import test, { describe } from 'node:test'; + +import { getPathAndLine } from '../../src/cli.js'; + +describe('CLI target parsing for path:line', () => { + const testParsing = ( + name: string, + input: string, + expectedPath: string | undefined, + expectedLine: number | undefined, + ) => { + return test(name, () => { + const { path, line } = getPathAndLine(input); + assert.strictEqual(path, expectedPath); + assert.strictEqual(line, expectedLine); + }); + }; + + testParsing('empty string', '', undefined, undefined); + + // paths without line + testParsing('simple path only', 'path/to/file', 'path/to/file', undefined); + testParsing('path with backslashes', 'path/to\\hehe/file', 'path/to\\hehe/file', undefined); + testParsing('path with backslashes', 'path/to\\\\hehe/file', 'path/to\\hehe/file', undefined); + testParsing('path with colon', 'path/to:hehe/file', 'path/to:hehe/file', undefined); + testParsing('path with escaped colon', 'path/to\\:hehe/file', 'path/to:hehe/file', undefined); + testParsing( + 'path with escaped colon and backslashes', + 'path/to\\:hehe\\\\\\/file', + 'path/to:hehe\\\\/file', + undefined, + ); + testParsing('path with colon at end', 'path/to/file:', 'path/to/file:', undefined); + testParsing('path with backslashes at end', 'path/to/file\\', undefined, undefined); + testParsing( + 'path with escaped backslashes at end', + 'path/to/file\\\\', + 'path/to/file\\', + undefined, + ); + testParsing('escaped colon suppressing line', 'path/to/file\\:1', 'path/to/file:1', undefined); + + // with line + testParsing('simple path with line', 'path/to/file:123', 'path/to/file', 123); + testParsing('colon path with line', 'path/to:hehe/file:123', 'path/to:hehe/file', 123); + testParsing( + 'escaped colon path with line', + 'path/to\\:hehe/file:123', + 'path/to:hehe/file', + 123, + ); + testParsing('colon at end with line', 'path/to/file::123', 'path/to/file:', 123); +}); diff --git a/tsconfig.json b/tsconfig.json index 7e79cc05..3a9572de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,6 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true - } + }, + "include": ["src/**/*"] } From 0e7fae1dd37d07b964aa07a7fbd1840c006302a2 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 30 Jul 2024 13:05:19 +0200 Subject: [PATCH 22/29] ci(#103): run tests --- .github/workflows/ci.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4be6c117..71ef2856 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,14 @@ jobs: - run: yarn - run: yarn lint + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: yarn + - run: yarn test + build-linux: name: Build Linux needs: [lint] From 6237788221b6ceb7010d48b1ebad6be1d6e102a9 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Tue, 30 Jul 2024 13:23:26 +0200 Subject: [PATCH 23/29] docs(#103): fix --help --- viv | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/viv b/viv index 365c55aa..2730aec7 100755 --- a/viv +++ b/viv @@ -2,10 +2,14 @@ print_usage() { cat < Date: Tue, 30 Jul 2024 16:11:16 +0200 Subject: [PATCH 24/29] docs(#103): fix comment --- src/parser/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/config.ts b/src/parser/config.ts index 54da20f2..117aa94a 100644 --- a/src/parser/config.ts +++ b/src/parser/config.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import { homedir } from 'os'; import path from 'path'; -// NOTE: this does type not directly correspond to the config file: see +// NOTE: this type does not directly correspond to the config file: see // defaultConfig, envConfigs and configFileBlocked type Config = { styles?: string; From 808cd179d49316aa123047b7dcf5517667255244 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Thu, 1 Aug 2024 12:06:01 +0200 Subject: [PATCH 25/29] feat(#103): more robust message queuing --- src/app.ts | 2 +- src/cli.ts | 23 ++++++++++++++++++----- src/routes/_open.ts | 25 +++++++++---------------- src/sockets.ts | 44 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/app.ts b/src/app.ts index 6920cf8f..3bfb7b39 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ app.use('/_open', openRouter); const server = createServer(app); let shutdownTimer: NodeJS.Timeout | null = null; -export const { clientsAt, messageClients, queueMessage, deleteQueuedMessage } = setupSockets( +export const { clientsAt, messageClients, openAndMessage } = setupSockets( server, () => { if (config.timeout > 0) diff --git a/src/cli.ts b/src/cli.ts index ae1b8ab1..18e7923c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,8 @@ import { existsSync } from 'fs'; import { resolve as presolve } from 'path'; import config from './parser/config.js'; import axios from 'axios'; +import { pathToURL, preferredPath } from './utils/path.js'; +import open from 'open'; export const address = `http://localhost:${config.port}`; @@ -18,6 +20,9 @@ export const getPathAndLine = ( return { path, line }; }; +export const openFileAt = async (path: string) => + open(`${address}${pathToURL(preferredPath(path))}`); + const openTarget = async (target: string) => { const { path, line } = getPathAndLine(target); if (!path) { @@ -30,11 +35,19 @@ const openTarget = async (target: string) => { } const resolvedPath = presolve(path); - await axios.post(`${address}/_open`, { - path: resolvedPath, - command: line !== undefined ? 'SCROLL' : undefined, - value: line, - }); + try { + if (line !== undefined) { + await axios.post(`${address}/_open`, { + path: resolvedPath, + command: 'SCROLL', + value: line, + }); + } else { + await openFileAt(resolvedPath); + } + } catch { + console.log(`Failed to open ${target}`); + } }; export const handleArgs = async () => { diff --git a/src/routes/_open.ts b/src/routes/_open.ts index 35a7a60f..8ac1212a 100644 --- a/src/routes/_open.ts +++ b/src/routes/_open.ts @@ -1,8 +1,6 @@ import { Request, Response, Router } from 'express'; -import { deleteQueuedMessage, queueMessage } from '../app.js'; -import { address } from '../cli.js'; -import { pathToURL, preferredPath } from '../utils/path.js'; -import open from 'open'; +import { openAndMessage } from '../app.js'; +import { openFileAt } from '../cli.js'; // this route should only be used internally between vivify processes export const router = Router(); @@ -15,20 +13,15 @@ router.post('/', async (req: Request, res: Response) => { return; } - // NOTE: if we ever want to properly consider having many clients to one - // server (currently not smart because entire file system would be - // exposed), we will have to protect this critical section between here and - // the websocket of the client connecting in `src/sockets.ts` - if (command) { - queueMessage(path, `${command}: ${value}`); - } else { - deleteQueuedMessage(path); - } - try { - await open(`${address}${pathToURL(preferredPath(path))}`); + if (command) { + await openAndMessage(path, `${command}: ${value}`); + } else { + await openFileAt(path); + } } catch { - deleteQueuedMessage(path); + res.status(500).end(); + return; } res.end(); diff --git a/src/sockets.ts b/src/sockets.ts index 447d6fe2..d40a2028 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -1,6 +1,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import { v4 as uuidv4 } from 'uuid'; import { Server } from 'http'; +import { openFileAt } from './cli.js'; interface SocketData { socket: WebSocket; @@ -13,8 +14,8 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const wss = new WebSocketServer({ server }); const sockets = new Map(); - // queue of initial message to be sent to new clients - const messageQueue = new Map(); + // queue of messages to be sent to clients after they have connected + const openQueue = new Map(); const terminateSocket = (id: string) => { const socket = sockets.get(id); @@ -46,11 +47,19 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli switch (key) { case 'PATH': sockets.get(id)!.path = value; - const message = messageQueue.get(value); - if (message) { - messageQueue.delete(value); - socket.send(message); + const queue = openQueue.get(value); + if (!queue) return; + + let message: string | undefined = undefined; + while (queue.length) { + const item = queue.shift(); + if (item && item.timeout > Date.now()) { + message = item.message; + break; + } } + if (!queue.length) openQueue.delete(value); + if (message) socket.send(message); break; } }); @@ -77,8 +86,25 @@ export function setupSockets(server: Server, onNoClients: () => void, onFirstCli const messageClients = (clients: SocketData[], message: string) => clients.forEach(({ socket }) => socket.send(message)); - const queueMessage = (path: string, message: string) => messageQueue.set(path, message); - const deleteQueuedMessage = (path: string) => messageQueue.delete(path); + // NOTE: The message queuing relies on the server running on the same + // machine that is used to view the files. if we ever want to consider + // having a "real" server with other machine(s) acting as client(s), we + // have to switch to query parameters instead of queued messages (this + // would currently not be smart anyways because the server's entire + // file system would be exposed). + // The reason we don't use query parameters is because this would not allow + // reusing the same tab when (re)opening a file on browsers that support it + // (e.g. Safari) + const openAndMessage = async (path: string, message: string) => { + const queue = openQueue.get(path); + const newItem = { message, timeout: Date.now() + 1000 }; + if (queue) { + queue.push(newItem); + } else { + openQueue.set(path, [newItem]); + } + await openFileAt(path); + }; - return { clientsAt, messageClients, queueMessage, deleteQueuedMessage }; + return { clientsAt, messageClients, openAndMessage }; } From 090d936a988c08664ca1442a64d49030e9163f85 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Thu, 1 Aug 2024 12:16:11 +0200 Subject: [PATCH 26/29] refactor(#103): should render switch --- src/parser/parser.ts | 3 +++ src/routes/viewer.ts | 17 ++++++++--------- src/utils/path.ts | 5 ----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 3fadbee3..a6ef7ec7 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -33,6 +33,9 @@ function textRenderer( } } +export const shouldRender = (mime: string): boolean => + mime.startsWith('text/') || mime === 'application/json'; + export function renderTextFile(content: string, path: string): string { const fileEnding = path?.split('.')?.at(-1); const renderInformation = textRenderer(fileEnding); diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index f0e5c262..b43e9297 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -6,8 +6,8 @@ import { Request, Response, Router } from 'express'; import { clientsAt, messageClients } from '../app.js'; import config from '../parser/config.js'; -import { absPath, isTextFile, pcomponents, preferredPath } from '../utils/path.js'; -import { renderDirectory, renderTextFile } from '../parser/parser.js'; +import { absPath, pcomponents, pmime, preferredPath } from '../utils/path.js'; +import { renderDirectory, renderTextFile, shouldRender } from '../parser/parser.js'; export const router = Router(); @@ -43,10 +43,9 @@ router.get(/.*/, async (req: Request, res: Response) => { body = renderDirectory(path); } else { const data = readFileSync(path); - const [isPlainText, type] = isTextFile(path); - - if (!isPlainText) { - res.setHeader('Content-Type', type).send(data); + const mime = pmime(path); + if (!shouldRender(mime)) { + res.setHeader('Content-Type', mime).send(data); return; } @@ -100,9 +99,9 @@ router.post(/.*/, async (req: Request, res: Response) => { let { content } = req.body; if (reload) { - const [isPlainText] = isTextFile(path); - if (!isPlainText) { - res.status(400).send('Reload is only permitted on plain text files'); + const mime = pmime(path); + if (!shouldRender(mime)) { + res.status(400).send('Reload is only permitted on rendered files'); return; } content = readFileSync(path).toString(); diff --git a/src/utils/path.ts b/src/utils/path.ts index aeb92082..38cd7e24 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -35,8 +35,3 @@ export const pathToURL = (path: string, route: string = 'viewer') => { export const preferredPath = (path: string): string => config.preferHomeTilde && path.startsWith(homedir()) ? path.replace(homedir(), '~') : path; - -export const isTextFile = (path: string): [boolean, string] => { - const type = pmime(path); - return [type.startsWith('text/') || type === 'application/json', type]; -}; From fbaed7e0b31023927c9119c9f997e8c99ccefdaf Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Thu, 1 Aug 2024 12:23:02 +0200 Subject: [PATCH 27/29] refactor(#103): cli --- src/app.ts | 4 ++-- src/cli.ts | 15 +++++++-------- src/parser/config.ts | 8 +++++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app.ts b/src/app.ts index 3bfb7b39..33d77f50 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,14 +2,14 @@ import { createServer, get } from 'http'; import express from 'express'; -import config from './parser/config.js'; +import config, { address } from './parser/config.js'; import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; import { router as openRouter } from './routes/_open.js'; import { setupSockets } from './sockets.js'; import { urlToPath } from './utils/path.js'; -import { address, handleArgs } from './cli.js'; +import { handleArgs } from './cli.js'; const app = express(); app.use(express.json()); diff --git a/src/cli.ts b/src/cli.ts index 18e7923c..b8959d82 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,11 @@ +import axios from 'axios'; import { existsSync } from 'fs'; +import open from 'open'; import { resolve as presolve } from 'path'; -import config from './parser/config.js'; -import axios from 'axios'; +import { address } from './parser/config.js'; import { pathToURL, preferredPath } from './utils/path.js'; -import open from 'open'; - -export const address = `http://localhost:${config.port}`; +// exported for unit test export const getPathAndLine = ( target: string, ): { path: string | undefined; line: number | undefined } => { @@ -53,13 +52,13 @@ const openTarget = async (target: string) => { export const handleArgs = async () => { try { const args = process.argv.slice(2); - const positional: string[] = []; + const positionals: string[] = []; let parseOptions = true; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (!(arg.startsWith('-') && parseOptions)) { - positional.push(arg); + positionals.push(arg); continue; } switch (arg) { @@ -74,7 +73,7 @@ export const handleArgs = async () => { console.log(`Unknown option "${arg}"`); } } - await Promise.all(positional.map((target) => openTarget(target))); + await Promise.all(positionals.map((target) => openTarget(target))); } finally { if (process.env['NODE_ENV'] !== 'development') { // - viv executable waits for this string and then stops printing diff --git a/src/parser/config.ts b/src/parser/config.ts index 117aa94a..acea939b 100644 --- a/src/parser/config.ts +++ b/src/parser/config.ts @@ -56,7 +56,7 @@ const getFileContents = (paths: string[] | string | undefined): string => { return getFileContent(paths); }; -const getConfig = (): Config => { +const config = ((): Config => { let config = undefined; // greedily find config for (const cp of configPaths) { @@ -90,6 +90,8 @@ const getConfig = (): Config => { if (process.env[env]) config[key] = process.env[env]; } return config; -}; +})(); + +export default config; -export default getConfig(); +export const address = `http://localhost:${config.port}`; From d5a7d3fbbcb9f7e82fccff8c7ad76f6380fdfc24 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Thu, 1 Aug 2024 12:28:30 +0200 Subject: [PATCH 28/29] ci(#103): fix node version for tests --- .github/workflows/ci.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 71ef2856..86b68298 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,12 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: set up node + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: checkout + uses: actions/checkout@v4 - run: yarn - run: yarn test From a21cc0522e8b29f56f236ff77dadb6eea5f2e4e6 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Thu, 1 Aug 2024 12:33:33 +0200 Subject: [PATCH 29/29] refactor(#103): move config.ts --- src/app.ts | 2 +- src/cli.ts | 2 +- src/{parser => }/config.ts | 0 src/parser/markdown.ts | 2 +- src/parser/parser.ts | 2 +- src/routes/viewer.ts | 2 +- src/utils/path.ts | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/{parser => }/config.ts (100%) diff --git a/src/app.ts b/src/app.ts index 33d77f50..e4b92a9f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,7 @@ import { createServer, get } from 'http'; import express from 'express'; -import config, { address } from './parser/config.js'; +import config, { address } from './config.js'; import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; diff --git a/src/cli.ts b/src/cli.ts index b8959d82..176fb03f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import { existsSync } from 'fs'; import open from 'open'; import { resolve as presolve } from 'path'; -import { address } from './parser/config.js'; +import { address } from './config.js'; import { pathToURL, preferredPath } from './utils/path.js'; // exported for unit test diff --git a/src/parser/config.ts b/src/config.ts similarity index 100% rename from src/parser/config.ts rename to src/config.ts diff --git a/src/parser/markdown.ts b/src/parser/markdown.ts index 04650c40..3768a728 100644 --- a/src/parser/markdown.ts +++ b/src/parser/markdown.ts @@ -3,7 +3,7 @@ import anchor from 'markdown-it-anchor'; import highlight from './highlight.js'; import graphviz from './dot.js'; import githubAlerts from 'markdown-it-github-alerts'; -import config from './config.js'; +import config from '../config.js'; import { Renderer } from './parser.js'; const mdit = new MarkdownIt({ diff --git a/src/parser/parser.ts b/src/parser/parser.ts index a6ef7ec7..ee6388c2 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -2,7 +2,7 @@ import { Dirent } from 'fs'; import { homedir } from 'os'; import { join as pjoin, dirname as pdirname, basename as pbasename } from 'path'; import { pathToURL } from '../utils/path.js'; -import config from './config.js'; +import config from '../config.js'; import renderNotebook from './ipynb.js'; import renderMarkdown from './markdown.js'; import { globSync } from 'glob'; diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index b43e9297..8ba4343e 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -5,7 +5,7 @@ import { homedir } from 'os'; import { Request, Response, Router } from 'express'; import { clientsAt, messageClients } from '../app.js'; -import config from '../parser/config.js'; +import config from '../config.js'; import { absPath, pcomponents, pmime, preferredPath } from '../utils/path.js'; import { renderDirectory, renderTextFile, shouldRender } from '../parser/parser.js'; diff --git a/src/utils/path.ts b/src/utils/path.ts index 38cd7e24..5e0cff13 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,7 +1,7 @@ import { execSync } from 'child_process'; import { homedir } from 'os'; import { basename as pbasename, dirname as pdirname, parse as pparse } from 'path'; -import config from '../parser/config.js'; +import config from '../config.js'; export const pmime = (path: string) => execSync(`file --mime-type -b '${path}'`).toString().trim();