Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API enhancements #117

Merged
merged 29 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7c57923
feat(#103): live reload
jannis-baum Jul 20, 2024
585f718
feat!(#103): remove port configurability in config file
jannis-baum Jul 22, 2024
94f6646
refactor(#103): scrollTo client function
jannis-baum Jul 22, 2024
bc10540
feat(#103): set initial scroll position by query
jannis-baum Jul 22, 2024
943dffa
feat(#103): remove query params on load
jannis-baum Jul 22, 2024
2941364
feat(#103): return number of clients on post/delete
jannis-baum Jul 22, 2024
6ea498c
feat(#103): open at scroll on same tab
jannis-baum Jul 22, 2024
3b38214
feat(#103): remove client on socket close
jannis-baum Jul 23, 2024
c76cd34
feat(#103): update help message
jannis-baum Jul 23, 2024
d421d6b
chore!(#103): remove query approach
jannis-baum Jul 27, 2024
4e4ec40
feat(#103): queue messages
jannis-baum Jul 27, 2024
ca23da9
feat(#103): scroll with message queuing
jannis-baum Jul 27, 2024
0daea67
chore(#103): comment & underscore prefix
jannis-baum Jul 27, 2024
308090f
feat(#103): clear queue
jannis-baum Jul 27, 2024
5d1553f
refactor(#103): handle queue+open server-side
jannis-baum Jul 27, 2024
efd41c9
feat(#103): add -- for targets starting in dashes
jannis-baum Jul 29, 2024
1669bbb
refactor(#103): --help with heredoc
jannis-baum Jul 30, 2024
59c73a5
docs(#103): update --help for :n suffix
jannis-baum Jul 30, 2024
0693d68
feat(#103): colon-based scrolling
jannis-baum Jul 30, 2024
42c9a56
refactor(#103): tests/rendering dir
jannis-baum Jul 30, 2024
ddceb98
feat(#103): test & fixes
jannis-baum Jul 30, 2024
0e7fae1
ci(#103): run tests
jannis-baum Jul 30, 2024
6237788
docs(#103): fix --help
jannis-baum Jul 30, 2024
bd4ceec
docs(#103): fix comment
jannis-baum Jul 30, 2024
808cd17
feat(#103): more robust message queuing
jannis-baum Aug 1, 2024
090d936
refactor(#103): should render switch
jannis-baum Aug 1, 2024
fbaed7e
refactor(#103): cli
jannis-baum Aug 1, 2024
d5a7d3f
ci(#103): fix node version for tests
jannis-baum Aug 1, 2024
a21cc05
refactor(#103): move config.ts
jannis-baum Aug 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ jobs:
- run: yarn
- run: yarn lint

test:
name: Test
runs-on: ubuntu-latest
steps:
- 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

build-linux:
name: Build Linux
needs: [lint]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ issue](https://github.com/jannis-baum/vivify/issues/new/choose) or
- `<kbd>` 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

Expand Down
4 changes: 2 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 14 additions & 5 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
"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": {
"@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",
Expand Down
50 changes: 6 additions & 44 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
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 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';
import { router as openRouter } from './routes/_open.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 { handleArgs } from './cli.js';

const app = express();
app.use(express.json());
Expand All @@ -21,11 +20,12 @@ app.use((req, res, next) => {
app.use('/static', staticRouter);
app.use('/health', healthRouter);
app.use('/viewer', viewerRouter);
app.use('/_open', openRouter);

const server = createServer(app);

let shutdownTimer: NodeJS.Timeout | null = null;
export const { clientsAt, messageClientsAt } = setupSockets(
export const { clientsAt, messageClients, openAndMessage } = setupSockets(
server,
() => {
if (config.timeout > 0)
Expand All @@ -39,44 +39,6 @@ export const { clientsAt, messageClientsAt } = 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();
Expand Down
85 changes: 85 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import axios from 'axios';
import { existsSync } from 'fs';
import open from 'open';
import { resolve as presolve } from 'path';
import { address } from './config.js';
import { pathToURL, preferredPath } from './utils/path.js';

// exported for unit test
export const getPathAndLine = (
target: string,
): { path: string | undefined; line: number | undefined } => {
const exp = /^(?<path>(?:.*?)(?<!\\)(?:\\\\)*)(?::(?<line>\d+))?$/;
const groups = target.match(exp)?.groups;
if (groups === undefined || !groups['path']) {
return { path: undefined, line: undefined };
}
const path = groups['path'].replace('\\:', ':').replace('\\\\', '\\');
const line = groups['line'] ? parseInt(groups['line']) : undefined;
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) {
console.log(`Invalid target: ${target}`);
return;
}
if (!existsSync(path)) {
console.log(`File not found: ${path}`);
return;
}

const resolvedPath = presolve(path);
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 () => {
try {
const args = process.argv.slice(2);
const positionals: string[] = [];
let parseOptions = true;

for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!(arg.startsWith('-') && parseOptions)) {
positionals.push(arg);
continue;
}
switch (arg) {
case '-v':
case '--version':
console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`);
break;
case '--':
parseOptions = false;
break;
default:
console.log(`Unknown option "${arg}"`);
}
}
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
// vivify-server's output and terminates
// - the string itself is not shown to the user
console.log('STARTUP COMPLETE');
}
}
};
19 changes: 16 additions & 3 deletions src/parser/config.ts → src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import fs from 'fs';
import { homedir } from 'os';
import path from 'path';

// NOTE: this type does not directly correspond to the config file: see
// defaultConfig, envConfigs and configFileBlocked
type Config = {
styles?: string;
scripts?: string;
Expand All @@ -18,18 +20,23 @@ 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'],
timeout: 10000,
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'),
Expand All @@ -49,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) {
Expand All @@ -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);
Expand All @@ -79,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}`;
2 changes: 1 addition & 1 deletion src/parser/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
5 changes: 4 additions & 1 deletion src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions src/routes/_open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, Response, Router } from 'express';
import { openAndMessage } from '../app.js';
import { openFileAt } from '../cli.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;
}

try {
if (command) {
await openAndMessage(path, `${command}: ${value}`);
} else {
await openFileAt(path);
}
} catch {
res.status(500).end();
return;
}

res.end();
});
Loading