#!/usr/bin/env node // scripts/vendor-playground-libs.mjs // Reproducible vendor script for v4.2 playground render-pipeline. // // Usage: node scripts/vendor-playground-libs.mjs // // Pins (locked per plan-critic B3 — never use highlightjs.org website builder // or any other interactive UI; this script is fully headless): // - markdown-it@14.1.0 (UMD bundle copied verbatim) // - markdown-it-front-matter@0.2.4 (CommonJS module wrapped in IIFE) // - highlight.js@11.11.1 (5-lang bundle assembled from CommonJS sources) // - dompurify@3.2.6 (UMD bundle copied verbatim) — v4.3 Step 24 // // Output: playground/lib/{markdown-it.min.js, markdown-it-front-matter.min.js, // highlight.min.js, dompurify.min.js} // // All three output files are zero-network browser-loadable scripts that // expose globals (`window.markdownit`, `window.markdownitFrontMatter`, // `window.hljs`). They also work under Node.js dynamic-import via the // pattern in scripts/render-artifact.mjs (UMD + global-eval). import { execSync } from 'node:child_process'; import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..'); const OUT = join(ROOT, 'playground', 'lib'); const PINS = { 'markdown-it': '14.1.0', 'markdown-it-front-matter': '0.2.4', 'highlight.js': '11.11.1', // v4.3 Step 24 — pinned ≥ 3.1.1 (PortSwigger HTML-comment mutation-XSS bypass // was fixed in 3.1.x; 3.2.6 is the current stable line as of 2026-05-10). 'dompurify': '3.2.6', }; const HL_LANGS = ['yaml', 'json', 'javascript', 'bash', 'markdown', 'diff']; function vendor() { mkdirSync(OUT, { recursive: true }); const tmp = mkdtempSync(join(tmpdir(), 'voyage-vendor-')); const log = (msg) => process.stdout.write(`[vendor] ${msg}\n`); try { // 1. markdown-it — copy UMD min bundle directly log('packing markdown-it@' + PINS['markdown-it']); execSync(`npm pack markdown-it@${PINS['markdown-it']} --silent`, { cwd: tmp }); execSync(`tar xzf markdown-it-${PINS['markdown-it']}.tgz`, { cwd: tmp }); copyFileSync( join(tmp, 'package', 'dist', 'markdown-it.min.js'), join(OUT, 'markdown-it.min.js'), ); log(`wrote ${join(OUT, 'markdown-it.min.js')}`); // 2. markdown-it-front-matter — wrap CommonJS in IIFE that exposes a global log('packing markdown-it-front-matter@' + PINS['markdown-it-front-matter']); execSync(`npm pack markdown-it-front-matter@${PINS['markdown-it-front-matter']} --silent`, { cwd: tmp }); execSync(`tar xzf markdown-it-front-matter-${PINS['markdown-it-front-matter']}.tgz`, { cwd: tmp }); const fmSrc = readFileSync(join(tmp, 'package', 'index.js'), 'utf-8'); const fmBundle = wrapCommonJS('markdownitFrontMatter', fmSrc); writeFileSync(join(OUT, 'markdown-it-front-matter.min.js'), fmBundle); log(`wrote ${join(OUT, 'markdown-it-front-matter.min.js')}`); // 3. highlight.js — assemble core + 5 languages from CommonJS sources log('packing highlight.js@' + PINS['highlight.js']); execSync(`npm pack highlight.js@${PINS['highlight.js']} --silent`, { cwd: tmp }); execSync(`tar xzf highlight.js-${PINS['highlight.js']}.tgz`, { cwd: tmp }); const coreSrc = readFileSync(join(tmp, 'package', 'lib', 'core.js'), 'utf-8'); const langSrcs = HL_LANGS.map((lang) => ({ lang, src: readFileSync(join(tmp, 'package', 'lib', 'languages', `${lang}.js`), 'utf-8'), })); const hlBundle = assembleHighlight(coreSrc, langSrcs); writeFileSync(join(OUT, 'highlight.min.js'), hlBundle); log(`wrote ${join(OUT, 'highlight.min.js')} (${HL_LANGS.length} langs)`); // 4. dompurify — copy UMD min bundle directly (v4.3 Step 24). // Mirrors markdown-it-vendoring: npm pack → tar xzf → copy // dist/purify.min.js → playground/lib/dompurify.min.js. The UMD bundle // exposes `window.DOMPurify` for browser-loadable use. log('packing dompurify@' + PINS['dompurify']); execSync(`npm pack dompurify@${PINS['dompurify']} --silent`, { cwd: tmp }); execSync(`tar xzf dompurify-${PINS['dompurify']}.tgz`, { cwd: tmp }); copyFileSync( join(tmp, 'package', 'dist', 'purify.min.js'), join(OUT, 'dompurify.min.js'), ); log(`wrote ${join(OUT, 'dompurify.min.js')}`); // 5. MANIFEST — record the vendored versions for audit const manifest = { generated_at: new Date().toISOString(), pins: PINS, highlight_languages: HL_LANGS, output_files: [ 'markdown-it.min.js', 'markdown-it-front-matter.min.js', 'highlight.min.js', 'dompurify.min.js', ], }; writeFileSync( join(OUT, 'VENDOR-MANIFEST.json'), JSON.stringify(manifest, null, 2) + '\n', ); log(`wrote ${join(OUT, 'VENDOR-MANIFEST.json')}`); } finally { rmSync(tmp, { recursive: true, force: true }); } log('done'); } /** * Wrap a CommonJS module body (uses `module.exports = ...`) in an IIFE * that exposes the export as a global on `window` (browser) or * `globalThis` (Node). */ function wrapCommonJS(globalName, src) { return [ `// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT`, `// global: ${globalName}`, `(function (root, factory) {`, ` var __mod = { exports: {} };`, ` (function (module, exports) {`, ` ${src.replace(/\n/g, '\n ')}`, ` })(__mod, __mod.exports);`, ` root[${JSON.stringify(globalName)}] = __mod.exports;`, `})(typeof window !== 'undefined' ? window : globalThis);`, ``, ].join('\n'); } /** * Assemble a self-contained highlight.js IIFE with core + N languages. * * Output exposes `window.hljs` (and `globalThis.hljs` under Node). */ function assembleHighlight(coreSrc, langSrcs) { const parts = [ `// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT`, `// global: hljs (highlight.js@${PINS['highlight.js']} — core + ${langSrcs.map(l => l.lang).join('/')})`, `(function (root) {`, ` function loadCommonJS(src) {`, ` var __mod = { exports: {} };`, ` var fn = new Function('module', 'exports', src);`, ` fn(__mod, __mod.exports);`, ` return __mod.exports;`, ` }`, ` var coreSrc = ${JSON.stringify(coreSrc)};`, ` var hljs = loadCommonJS(coreSrc);`, ]; for (const { lang, src } of langSrcs) { parts.push(` var lang_${lang.replace(/\W/g, '_')} = loadCommonJS(${JSON.stringify(src)});`); parts.push(` hljs.registerLanguage(${JSON.stringify(lang)}, lang_${lang.replace(/\W/g, '_')});`); } parts.push(` root.hljs = hljs;`); parts.push(`})(typeof window !== 'undefined' ? window : globalThis);`); parts.push(''); return parts.join('\n'); } if (import.meta.url === `file://${process.argv[1]}`) { vendor(); } export { vendor, wrapCommonJS, assembleHighlight };