Skip to content

Instantly share code, notes, and snippets.

@agoose77
Last active August 13, 2025 11:23
Show Gist options
  • Save agoose77/6cbe969a88568dfcf78f0457fd9a0f4e to your computer and use it in GitHub Desktop.
Save agoose77/6cbe969a88568dfcf78f0457fd9a0f4e to your computer and use it in GitHub Desktop.
  1. Run npm install
  2. Run npm run build
  3. Use node dist/provision.cjs **/*.md inside your top-level Python virtual environment that is used by MyST to run Jupyter Server
#!/usr/bin/env node
import { load } from "js-toml";
import { selectAll } from "unist-util-select";
import { join, resolve } from "node:path";
import { execSync } from "node:child_process";
import { readFileSync, existsSync, rmdirSync, mkdirSync } from "node:fs";
import { mystParse } from "myst-parser";
import { getFrontmatter } from "myst-transforms";
import { parse as parseYaml } from "yaml";
import { VFile } from "vfile";
const REGEX =
/^# \/\/\/ (?<type>[a-zA-Z0-9-]+)$\s(?<content>(^#(| .*)$\s)+)^# \/\/\/$/gm;
function parseScriptMetadata(script) {
const name = "script";
const matches = Array.from(
script.matchAll(REGEX).filter((m) => m.groups["type"] === name),
);
if (matches.length > 1) {
throw new Error(`Multiple ${name} blocks found`);
} else if (matches.length === 1) {
const content = matches[0].groups["content"]
.split(/\r?\n/)
.map((line) => (line.startsWith("# ") ? line.slice(2) : line.slice(1)))
.join("\n");
return load(content);
} else {
return undefined;
}
}
const VENV_PATH = resolve(".myst-venvs");
function getPythonPrefix() {
const result = execSync("python -m sysconfig");
const stdout = result.toString();
const schemeMatch = /^.*installation scheme:\s*"venv"/m.test(stdout);
if (!schemeMatch) {
console.warn("Expected virtual environment scheme");
}
const match = stdout.match(/^\s*data\s*=\s*"([^"]+)"\s*$/m);
return match[1];
}
function findEnvironmentSpec(mdast) {
// Find possible script definitions
const spec = selectAll("code", mdast)
.map((n) => parseScriptMetadata(n.value))
.find((n) => n);
return spec;
}
function buildEnvironment(kernelName, dependencies, envPath, rootPrefix) {
const interpreter = join(rootPrefix, "bin", "python");
//Provision venv
execSync(`${interpreter} -m venv ${envPath}`);
const venvInterpreter = join(envPath, "bin", "python");
// Install deps
const dependenciesToken = dependencies.map((token) => `"${token}"`).join(" ");
execSync(`${venvInterpreter} -m pip install ${dependenciesToken}`);
// Install kernelspec to root env
execSync(
`${venvInterpreter} -m ipykernel install --name ${kernelName} --prefix ${rootPrefix}`,
);
}
function main() {
if (existsSync(VENV_PATH)) {
rmdirSync(VENV_PATH, { recursive: true });
}
const paths = process.argv.slice(2).map((name) => resolve(name));
const pythonPrefix = getPythonPrefix();
const parser = (content) =>
mystParse(content, { extensions: { frontmatter: true } });
for (const p of paths) {
const content = readFileSync(p, { encoding: "utf-8" });
// Parse the Markdown
const rawMdast = parser(content);
// Extract the AST
const { tree: mdast, frontmatter } = getFrontmatter(new VFile(), rawMdast);
// Ensure we have a kernel name
const kernelName = frontmatter?.kernelspec?.name;
if (kernelName === undefined) {
console.debug(`Skipping ${p}, missing kernel name`);
continue;
}
// Define dependencies
const spec = findEnvironmentSpec(mdast);
if (spec === undefined) {
console.debug(`Skipping ${p}, missing spec`);
continue;
}
const dependencies = [...spec.dependencies, "ipykernel"];
// Create venv
const envPath = join(VENV_PATH, kernelName);
mkdirSync(envPath, { recursive: true });
console.log("Building environment");
buildEnvironment(kernelName, dependencies, envPath, pythonPrefix);
}
}
main();
{
"type": "module",
"dependencies": {
"js-toml": "^1.0.2",
"myst-parser": "^1.5.15",
"myst-transforms": "^1.3.38",
"unist-util-select": "^4.0.3",
"vfile": "^6.0.3",
"yaml": "^2.8.1"
},
"devDependencies": {
"esbuild": "^0.25.4"
},
"scripts": {
"build": "esbuild provision.mjs --bundle --outfile=dist/provision.cjs --format=cjs --platform=node"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment