- Run
npm install
- Run
npm run build
- Use
node dist/provision.cjs **/*.md
inside your top-level Python virtual environment that is used by MyST to run Jupyter Server
Last active
August 13, 2025 11:23
-
-
Save agoose77/6cbe969a88568dfcf78f0457fd9a0f4e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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