More documentation, almost done with everything in lib.

main
Zed A. Shaw 1 year ago
parent fcb4c232a7
commit 5dfa9ebc6b
  1. 4
      commands/templates/secrets/config.json
  2. 146
      lib/config.js
  3. 17
      lib/devtools.js
  4. 156
      lib/docgen.js
  5. 2
      package.json

@ -19,8 +19,6 @@
"auth": "your BTC pay auth key",
"disabled": true
},
"canary_payments": "CANARY",
"base_host": "http://127.0.0.1:5001",
"bull_config": {
@ -36,8 +34,6 @@
"cookie_secret": "some_secret"
},
"payments_allow_zero": false,
"config_canary": "CANARY",
"loader": {
"root_db": "../ljsthw-private/db",
"tracker": "localhost:9001"

@ -1,6 +1,23 @@
/*
This is mostly a convenience that translates the `secrets/config.json` file into
easily accessible imports for the code. This file should be safe to commit to
git, but __do not__ commit `secrets/config.json` to git. At the same time, do not
put secrets directly in here. Instead, put them in `secrets/config.json` then
convert it to a variable here.
The primary purpose of this file is to make it easier to safely import configurations,
and to document what those configurations should be. You can't put documentation on
a `config.json` file but you can put it in `config.js`, so that's what I did.
The live configuration is in `secrets/config.json`, and that file is created from the
templates in `commands/templates/secrets/config.json`.
*/
import assert from "assert";
import fs from "fs";
/*
The path to the secret config.json file.
*/
export const CONFIG = "./secrets/config.json"
const load_config = () => {
@ -12,25 +29,144 @@ const load_config = () => {
}
}
/*
All of the secrets directly available. You can just impor this and
access what you want instead of using the variables below. Very helpful
if you just added something to the `secrets/config.json` but haven't
modified this file yet.
*/
export const secrets = load_config();
assert(secrets !== undefined, `Problem loading config file ${CONFIG}`);
// TODO: migrate away from these but for now need them to keep the old code working
/*
The paypal configuration used in `api/payments/paypal.js` and `client/components/Paypal.svelte`.
The configuration is an object with:
+ `"email"`: `string` of your Paypal email. ___NOT PUBLIC___
+ `"client_id"`: `string` of your Paypal ___PUBLIC___ client id. This is exposed to the user, but ___NOT__ the `secret`.
+ `"secret"`: `string` of your Paypal __PRIVATE__ id. __NEVER__ put the in a page in `client`.
+ `"disabled"`: `boolean` to indicate if this is disabled or not.
___FOOTGUN___: You will have a very bad time if the `"secret"` key is placed
in any page that's accessible on the internet. With that someone else can
drain your account, do charges, and many other things. Consider creating a
restricted API key if you can instead of a full service one, to reduce your
risk.
*/
export const paypal_private = secrets.paypal_private;
/*
Your BTCPayServer configuration. This uses the BTCPayServer "greenfield" API, which is
easier to use than the old one. This is used by `api/payments/btcpay.js` and `client/components/BTCPay.svelte`.
+ `"url"`: `string` of your store's URL, something like `"https://btctest.learnjsthehardway.com/api/v1/stores/HEXSTRING"`.
+ `"auth"`: `string` Your basic auth credentials of the form "Basic BASE64HEX=". ___NOT PUBLIC___.
+ `"disabled"`: `boolean` If this is disabled or not.
___FOOTGUN___: Do not put the `"auth"` on the internet as it should be private and only used by the `api/payments/btcpay.js`.
*/
export const btcpay_private = secrets.btcpay_private;
/*
Stripe configuration used by `api/payments/stripe.js`, `api/payments/stripe_webhook.js` and `client/components/Stripe.svelte`.
+ '"email"': `string` Your Stripe email. __NOT PUBLIC__? I'm not sure about this.
+ '"client_id"': `string` Your Stripe public key. __PUBLIC__ and given to the browser.
+ '"secret"': `string` Your Stripe ___SECRET__ key. __NOT PUBLIC__. Do not expose this.
+ '"endpoint_secret"': `string` Shared secret with Stripe and used in `api/payments/stripe_webhook.js` to validate Stripe's webhook callbacks. __NOT PUBLIC__
+ '"api_version"': `string` The version of the API that should be used. Currently "2022-08-01".
+ '"disabled"': `boolean` Whether Stripe is disabled or not.
___FOOTGUN___: Do not expose the `endpoint_secret`, `secret`, or `email` to the internet.
*/
export const stripe_private = secrets.stripe_private;
export const canary_payments = secrets.canary_payments;
/*
Used in redirects and some links. Should be the __entire__ base URL, including ports and
protocols __without__ a trailing slash. For example:
+ "base_host": "http://127.0.0.1:5001"
+ "base_host": "https://learnjsthehardway.com"
*/
export const base_host = secrets.base_host;
/*
Configuration for the bull queue. It should just be the configuration for your
Redis server, which is usually this:
```javascript
"bull_config": {
"redis": {
"host": "localhost",
"port": 6379,
"db": 5
}
},
```
Refer to the [Bull Documentation](https://github.com/OptimalBits/bull) for more.
*/
export const bull_config = secrets.bull_config;
export const config_canary = secrets.config_canary;
export const payments_allow_zero = secrets.payments_allow_zero;
/*
A `string` of either "redis" or "memory" for either a RedisStore, or
MemoryStore in Passport.js. Used in `lib/auth.js`.
*/
export const session_store = secrets.session_store;
/*
Used by the `commands/media.js` loader to process media files and configure the .torrent file generation. It's _kind_ of specific to my setup but study the `commands/media.js` file for what I do.
+ `"root_db"`: This is my private source database, which is a directory of `.md` and `.js` files I use to build my content.
+ `"tracker"`: Currently the Tracker WebSocket URL. For dev that's `"ws://localhost:9001"`, and in production it's `wss://learnjsthehardway.com/tracker` so you get SSL WebSockets.
*/
export const loader = secrets.loader;
/*
This configures the cookies in the `commands/api.js` for the web browser's cookies.
+ `"cookie_domain"`: "127.0.0.1" for development, the __exact__ protocol://host:port for your cookies.
+ `"cookie_secret"`: "changeme" for development but ___CHANGE THIS NOW___.
___FOOTGUN___: Cookies are so messed up that it's hard to debug why they fail. If you find that you can't log in to the server, or that your login disappears after you refresh then:
1. Make sure you aren't using MemoryStore configuration using `secrets.auth = "memory"`. This will make it forget your login if the server restarts, which you might think is from browser refresh.
2. Add `--cookies-suck` to `node bando.js api` so it will dump what it thinks your cookies are and what they should be.
3. Look at the Inspector window in your browser for the Network tab and see what it's sending as the cookie, if any.
The best advice is that this setting must be exactly the same as what the browser sees. Protocol, port, hostname, everthing must be the same.
*/
export const auth = secrets.auth;
/*
During deployment I do a simple `rsync` of media to additional media servers. This makes a
simple dirt cheap CDN when combined with WebTorrent. These will be added to the `.torrent`
files for each media in `commands/media.js` so that the browser will pull content from all
servers at the same time.
+ `["string", "string"]` -- A list of full URLs to each media server, as they should be in the `.torrent`.
*/
export const media_servers = secrets.media_servers;
/*
The `queues/discord.js` and `api/discord.js` provide a very simple Discord integration where
people can request a key after agreeing to a Code of Conduct, then join a discord. It's
_VERY_ simple, so look in the code to see how it works.
+ `"token"`: `string` Your discord key. __PRIVATE__
+ `"time_limit"`: `"24 hours"` Verbal description of the time limit, used in emails and pages for the user to know how long they have to claim an invite.
+ `"invites"`: `{ "maxAge": 86400, "maxUses": 1, "unique": true}` How the invites work. I suggest these options so you can map one invite to one user.
*/
export const discord = secrets.discord;
/*
Used by some services when talking to `socket/` handlers for events.
It's mostly a security mechanism to make sure only our code is sending certain
events and should be a giant cryptographically random key.
+ `"api_key"`: "AHEXCODE" make it big and truly random.
*/
export const socket = secrets.socket;
export const sync = secrets.sync;
export default secrets;

@ -1,9 +1,24 @@
/*
Contains shared variables with `api/devtools/info.js` and `commands/api.js` that
creates the display in the admin [errors](http://127.0.0.1:5001/admin/#/errors/) and
[routes](http://127.0.0.1:5001/admin/#/routes/) viewers.
*/
/*
All registered `api/` handlers.
*/
export const api = {};
/*
All registered `sockets/` handlers.
*/
export const sockets = {};
/*
All errors found in `debug/*_errors.json`. These are generated by the [esbuild](https://esbuild.github.io)
run done by `commands/build.js`. Then when `commands/api.js` runs it loads
this for you to view and notifies your browser there's errors.
*/
export const errors = [];
export default {

@ -1,3 +1,12 @@
/*
Utilities mostly for generating the documentation and static pages in
most sites. It's targetted at the kind of content I make, which is
programming docs, educational lessons, and other Markdown documents
with code in them.
The main feature of this module is the `class Ork` which load code from
external code files for inclusion in the output HTML.
*/
import { Remarkable } from "remarkable";
import assert from "assert";
// Node is absolutely stupid. The remarkable project has an esm setting which
@ -12,10 +21,38 @@ import child_process from "child_process";
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
class Ork {
/*
This is a little class I use to read in an external code
file, then wrap that code with the markdown code blocks.
Look in `render_with_code` to see how it is implemented:
1. Creates a `lodash` template that has a variable named `ork`.
2. When `render_with_code` runs it then first runs your `.md` file through this template.
3. That template can then call functions on `Ork`, as in: `{{ ork.js() }}`.
4. `Ork` then returns whatever Markdown code format is needed for the output.
*/
export class Ork {
/*
Given a `base` directory and metadata object it will load code as needed. Keep
in mind this is tied to my setup where I have a private directory in a separate
repository that contains the code for the course. This let's you do this:
```javascript
let ork = new Ork("javascript_level_1", { code: "exercise_1"});
let md_out = ork.js("code.js");
```
This will load the file in `javascript_level_1/exercise_1/code.js`, wrap it with
Markdown code blocks.
+ `base string` -- The base directory where all of the code examples are.
+ `metadata Object` -- When a `.md` file is loaded it has metadata at the top, and one is `code` which says where code for that `.md` file should be loaded.
___BUG___: `base` needs to go away, but for now it's where the root of the db is.
*/
constructor(base, metadata) {
// base needs to go away, but for now it's where the root of the db is
if(metadata.code) {
this.code_path = `${base}${metadata.code}`;
} else {
@ -23,11 +60,26 @@ class Ork {
}
}
/*
Loads a JavaScript file and returns it wrapped in a Markdown code block tagged
with `javascript`.
+ `file string` -- The file to load relative to `base/{metadata.code}` from the constructor.
*/
js(file='code.js') {
assert(this.code_path, "You are using ork but no code path set in metadata.");
return `\`\`\`javascript\n${fs.readFileSync(`${this.code_path}/${file}`).toString().trim()}\n\`\`\``
}
/*
Actually __executes__ the code file using the local `node` interpreter, and returns
the output, instead of the code. This lets you automatically generate the results
of code for your documents from one source. Otherwise you have to do this manually
then include it.
+ `file string` -- The file to _run_ relative to `base/{metadata.code}` from the constructor.
+ `args string` -- Arguments to the `node` command line.
*/
node(file='code.js', args='') {
assert(this.code_path, "You are using ork but no code path set.");
let cmd = `node "${file}" ${args}`;
@ -36,23 +88,35 @@ class Ork {
}
}
const count_lines = (str) => {
/*
To make CSS line numbers work you have to do this janky line counting
thing. Definitely not an efficient way to do it but it is a simple way.
*/
export const count_lines = (str) => {
let match = str.match(/\n(?!$)/g);
return match ? match.length + 1 : 1;
}
const line_number_spans = (str) => {
/*
Using `count_lines` this will generate a bunch of `<span></span>` tags
to match the count. This is the easiest way to create a strip of line
numbers to the left of a code block.
*/
export const line_number_spans = (str) => {
let count = count_lines(str);
let spans = new Array(count + 1).join('<span></span>');
return `<span aria-hidden="true" class="line-numbers-rows">${spans}</span>`;
}
/**
* Runs Prism on the string, defaulting to the "markup"
* language which will encode most code correctly even
* if you don't specify a language after the ```
**/
const run_prism = (str, lang="markup") => {
/*
Runs Prism on the string, defaulting to the "markup" language which will
encode most code correctly even if you don't specify a language after the
Markdown code block header.
+ `str string` -- Code to parse with Prism.
+ `lang string` -- Defaults to "markup" but can be anything Prism supports.
*/
export const run_prism = (str, lang="markup") => {
assert(lang !== undefined, "Language can't be undefined.");
// do the check again since the language might not exist
@ -64,17 +128,53 @@ const run_prism = (str, lang="markup") => {
}
}
/*
Does a complete highlight of a block of code for the specified
`lang`. It does the combination of `run_prism` + `line_number_spans`
so that the output is immediately useable as code output with line numbers.
+ `str string` -- Code to parse with Prism.
+ `lang string` -- Can be anything Prism supports, like "javascript" or "shell".
*/
export const highlight = (str, lang) => {
return run_prism(str, lang) + line_number_spans(str);
}
/* A separate renderer just for the titles that doesn't need anything else. */
/*
A separate renderer just for the titles that doesn't need anything else.
The main thing this does is turn of paragraphs by assigning:
```javascript
rem.renderer.rules.paragraph_open = pass;
rem.renderer.rules.paragraph_close = pass;
```
The `pass` function is an empty function that removes the paragraphs from the
Remarkable output. That's all you need for title rendering.
*/
export const title_render = new Remarkable('full').use(rem => {
let pass = (tokens, idx) => '';
rem.renderer.rules.paragraph_open = pass;
rem.renderer.rules.paragraph_close = pass;
});
/*
My Markdown files `.md` files with a format of:
```markup
{
metadata
}
------
Markdown
```
This function splits the input at the 6 `-` characters, runs
`JSON.parse` on the top part, then returns that metadata and body.
+ `raw_md string` -- The markdown to split.
+ ___return `Array[Object, string]` -- An Array with the metadata object and the string body.
*/
export const split = (raw_md) => {
let [metadata, ...body] = raw_md.split('------');
metadata = JSON.parse(metadata);
@ -84,6 +184,15 @@ export const split = (raw_md) => {
const null_cb = (metadata, body) => body;
/*
Constructs the Remarkable render function that will do:
1. `linkify` any hostnames it finds.
2. Update the `toc` Array with new headings for a TOC output.
3. Fix up links, but I'm not sure why I'm doing this.
___BUG___: Why am I changing the link output.
*/
export const create_renderer = (toc) => {
const renderer = new Remarkable('full', {
html: true,
@ -122,6 +231,18 @@ export const create_renderer = (toc) => {
return renderer;
}
/*
Performs a render of the `raw_md` that will:
1. Split the `raw_md` with `split`.
2. Render with a Remarkable renderer from `create_renderer`.
3. Go through each element of the TOC and run `title_render.render` on them.
+ `raw_md string` -- The Markdown string to parse.
+ `cb function(metadata, body)` -- An optional callback that gets the `metadata, body` from `split`.
+ ___return___ `{toc: Array, content: string, metadata: Object}`
*/
export const render = (raw_md, cb=null_cb) => {
let toc = [];
let [metadata, body] = split(raw_md);
@ -144,6 +265,19 @@ export const render = (raw_md, cb=null_cb) => {
return {toc, content, metadata};
}
/*
The tip of the iceberg, this does the actual render of a markdown file
with source code in it. It combines all of the functions so far into
one cohesive rendering function:
1. It uses `Ork` to load code into the markdown.
2. It uses `render` to create a Remarkable renderer.
3. Finally uses `slugify` to generate a slug for the metadata output.
+ `source_dir string` -- Source code directory for the `Ork(source_dir, metadata)` call.
+ `md_file string` -- Path to the markdown file to load.
+ ___return___ `{content: string, toc: Array, metadata: Object}`
*/
export const render_with_code = (source_dir, md_file) => {
// get the file without path or extension
const tail_no_ext = PATH.basename(md_file, PATH.extname(md_file));

@ -17,7 +17,7 @@
"api": "nodemon ./bando.js api",
"queue": "nodemon ./bando.js queue",
"test": "npx ava tests/**/*.js",
"docs": "nodemon bando.js codedoc --output public/docs/api lib/*.js client/*.js commands/*.js api/*.js api/**/*.js queues/*.js socket/*.js",
"docs": "nodemon bando.js codedoc --output public/docs/api 'lib/*.js' 'client/*.js' 'commands/*.js' 'api/*.js' 'api/**/*.js' 'queues/*.js' 'socket/*.js'",
"modules": "./bando.js load",
"modules-watch": "nodemon --watch ../ljsthw-private/db/modules/ --ext .md,.js,.sh ./bando.js load",
"rendered-watch": "nodemon --ignore \"rendered/build/**/*\" --watch ./rendered --watch static -e md,svelte,js,css ./bando.js rendered",

Loading…
Cancel
Save