mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2026-08-24 10:14:12 -05:00
feat(website): live-reload themes and wasm during local dev
`npm start` now runs scripts/dev.mjs instead of `docusaurus start` directly. It wraps the dev server with watchers that keep generated artifacts current without a manual regen + restart: - a /themes edit re-renders just that theme and patches it into generated/themes.json, so <ThemeGallery/> hot-reloads it - editing export_themes.mjs, font-metrics.mjs or segment_data.json falls back to a full `npm run themes`, since those can change every theme`s render - editing a Go source file under src/ rebuilds the studio`s wasm module (`npm run wasm`) `npm run build` and CI are unaffected: they already run these scripts explicitly before building and never watch anything. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Entire-Checkpoint: 23a2e79bf51a
This commit is contained in:
committed by
Jan De Dobbeleer
co-authored by
Copilot App
parent
47f2228931
commit
48d6685d85
@@ -384,6 +384,7 @@ if (process.argv[1] === __filename) {
|
||||
}
|
||||
|
||||
export {
|
||||
CONFIG,
|
||||
exportTheme,
|
||||
isValidTheme,
|
||||
getThemeNameFromFile,
|
||||
|
||||
Generated
+1
@@ -20,6 +20,7 @@
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prestart": "node scripts/ensure-artifacts.mjs",
|
||||
"start": "cross-env NODE_ENV=development docusaurus start --poll 1000",
|
||||
"start": "cross-env NODE_ENV=development node scripts/dev.mjs",
|
||||
"prebuild": "node scripts/ensure-artifacts.mjs",
|
||||
"build": "docusaurus build",
|
||||
"serve": "docusaurus serve",
|
||||
@@ -26,6 +26,7 @@
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env node
|
||||
// `npm start` runs this instead of `docusaurus start` directly, so local edits are reflected
|
||||
// live without restarting the dev server:
|
||||
//
|
||||
// - Editing/adding/removing a file under /themes re-renders *only that theme* and patches it
|
||||
// into generated/themes.json - the file <ThemeGallery/> statically imports, so webpack's own
|
||||
// dev-server watcher (already watching every file in the module graph) picks up the rewrite
|
||||
// and hot-reloads the gallery page. A full `npm run themes` re-renders all 124+ themes, one
|
||||
// child-process spawn each; patching just the changed entry turns a multi-second wait after
|
||||
// every save into a single render.
|
||||
// - Editing export_themes.mjs, font-metrics.mjs or segment_data.json - anything that can change
|
||||
// every theme's render, not just one - falls back to a full `npm run themes`.
|
||||
// - Editing a Go source file under src/ rebuilds the studio's wasm module (`npm run wasm`), so
|
||||
// a renderer change shows up in the in-browser studio without restarting anything.
|
||||
//
|
||||
// None of this runs for `npm run build` or CI: both already run `npm run themes` / `npm run
|
||||
// wasm` explicitly before building (see ensure-artifacts.mjs's own comment on that split), and
|
||||
// neither watches anything.
|
||||
//
|
||||
// A Node script rather than a second npm script run via a process-concurrency package: this
|
||||
// repo's developers run PowerShell, cmd and POSIX shells (see ensure-artifacts.mjs's own comment
|
||||
// on the same point), and spawning/forwarding signals by hand here avoids a new dependency for
|
||||
// what is otherwise a few lines of child_process.
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import chokidar from 'chokidar';
|
||||
|
||||
import { CONFIG, exportTheme, getThemeNameFromFile, isValidTheme } from '../export_themes.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const WEBSITE_DIR = join(__dirname, '..');
|
||||
const REPO_DIR = join(WEBSITE_DIR, '..');
|
||||
const SRC_DIR = join(REPO_DIR, 'src');
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// usePolling mirrors `docusaurus start --poll` below: some developers run this repo from
|
||||
// filesystems (network drives, some container/VM setups) where native OS file-change events
|
||||
// never fire, and every watcher here needs to behave the same way or only some of them would
|
||||
// work there.
|
||||
const POLLING_OPTIONS = { usePolling: true, interval: 1000 };
|
||||
|
||||
async function readManifest() {
|
||||
try {
|
||||
return JSON.parse(await readFile(CONFIG.MANIFEST_FILE, 'utf8'));
|
||||
} catch {
|
||||
// Missing or malformed: ensure-artifacts.mjs (prestart) already ran `npm run themes` once,
|
||||
// so this only happens if that file was deleted mid-session. Starting from empty is fine -
|
||||
// the next full regen or per-theme reload rebuilds it.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function writeManifest(manifest) {
|
||||
await mkdir(dirname(CONFIG.MANIFEST_FILE), { recursive: true });
|
||||
await writeFile(CONFIG.MANIFEST_FILE, JSON.stringify(manifest));
|
||||
}
|
||||
|
||||
// Reruns exactly one theme's render and splices the result into the existing manifest, in place
|
||||
// when the theme already has an entry (the common case: an edit) or appended and re-sorted when
|
||||
// it doesn't (a new theme file). Sorting only applies to inserts, so this can drift from
|
||||
// export_themes.mjs's own filename-sorted order over a long dev session of adds/renames - a full
|
||||
// `npm run themes` (which every commit's CI already runs) is the source of truth for that order.
|
||||
async function reloadTheme(themeFile) {
|
||||
console.log(`[themes] ${themeFile} changed, re-rendering...`);
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await exportTheme(themeFile);
|
||||
} catch (error) {
|
||||
console.error(`[themes] Failed to render ${themeFile}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = await readManifest();
|
||||
const index = manifest.findIndex((entry) => entry.name === result.entry.name);
|
||||
|
||||
if (index === -1) {
|
||||
manifest.push(result.entry);
|
||||
manifest.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} else {
|
||||
manifest[index] = result.entry;
|
||||
}
|
||||
|
||||
await writeManifest(manifest);
|
||||
console.log(`[themes] Updated "${result.entry.name}" in the gallery.`);
|
||||
}
|
||||
|
||||
async function removeTheme(themeFile) {
|
||||
const themeName = getThemeNameFromFile(themeFile);
|
||||
const manifest = await readManifest();
|
||||
const next = manifest.filter((entry) => entry.name !== themeName);
|
||||
|
||||
if (next.length === manifest.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeManifest(next);
|
||||
console.log(`[themes] Removed "${themeName}" from the gallery (file deleted).`);
|
||||
}
|
||||
|
||||
function runNpmScript(script) {
|
||||
const result = spawnSync('npm', ['run', script], { cwd: WEBSITE_DIR, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
|
||||
if (result.status === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error(`[dev] npm run ${script} exited with code ${result.status}. Fix the error above and save again.\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
function fullThemesRegen() {
|
||||
console.log('\n[themes] export_themes.mjs / font-metrics.mjs / segment_data.json changed, regenerating the full gallery...');
|
||||
|
||||
if (runNpmScript('themes')) {
|
||||
console.log('[themes] Theme gallery regenerated.\n');
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildWasm() {
|
||||
console.log('\n[wasm] Go source changed, rebuilding the studio wasm module...');
|
||||
|
||||
if (runNpmScript('wasm')) {
|
||||
console.log('[wasm] omp.wasm rebuilt.\n');
|
||||
}
|
||||
}
|
||||
|
||||
// A tiny per-key debounce/serialize queue: coalesces rapid-fire events for the same key (a save
|
||||
// that touches a file twice, or an editor writing a temp file then renaming it over the
|
||||
// original) into one run, and never overlaps two runs for the same key.
|
||||
function createDebouncedRunner(delayMs) {
|
||||
const timers = new Map();
|
||||
const running = new Set();
|
||||
const rerun = new Map();
|
||||
|
||||
function execute(key, fn) {
|
||||
if (running.has(key)) {
|
||||
rerun.set(key, fn);
|
||||
return;
|
||||
}
|
||||
|
||||
running.add(key);
|
||||
|
||||
Promise.resolve()
|
||||
.then(fn)
|
||||
.catch((error) => console.error(`[dev] ${error.message}`))
|
||||
.finally(() => {
|
||||
running.delete(key);
|
||||
|
||||
const next = rerun.get(key);
|
||||
|
||||
if (next) {
|
||||
rerun.delete(key);
|
||||
execute(key, next);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function schedule(key, fn) {
|
||||
clearTimeout(timers.get(key));
|
||||
timers.set(key, setTimeout(() => execute(key, fn), delayMs));
|
||||
};
|
||||
}
|
||||
|
||||
const scheduleThemeWork = createDebouncedRunner(300);
|
||||
const scheduleSharedWork = createDebouncedRunner(500);
|
||||
const scheduleWasmWork = createDebouncedRunner(500);
|
||||
|
||||
const themesWatcher = chokidar.watch(CONFIG.THEMES_CONFIG_DIR, { ignoreInitial: true, ...POLLING_OPTIONS });
|
||||
|
||||
themesWatcher.on('all', (event, path) => {
|
||||
const themeFile = path.split(/[/\\]/).pop();
|
||||
|
||||
if (!isValidTheme(themeFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'unlink') {
|
||||
scheduleThemeWork(themeFile, () => removeTheme(themeFile));
|
||||
} else if (event === 'add' || event === 'change') {
|
||||
scheduleThemeWork(themeFile, () => reloadTheme(themeFile));
|
||||
}
|
||||
});
|
||||
|
||||
const sharedWatcher = chokidar.watch(
|
||||
[join(WEBSITE_DIR, 'export_themes.mjs'), join(WEBSITE_DIR, 'font-metrics.mjs'), CONFIG.SEGMENT_DATA_FILE],
|
||||
{ ignoreInitial: true, ...POLLING_OPTIONS },
|
||||
);
|
||||
|
||||
sharedWatcher.on('all', () => scheduleSharedWork('themes', fullThemesRegen));
|
||||
|
||||
const wasmWatcher = chokidar.watch([join(SRC_DIR, '**/*.go'), join(SRC_DIR, 'go.mod'), join(SRC_DIR, 'go.sum')], {
|
||||
ignored: '**/*_test.go',
|
||||
ignoreInitial: true,
|
||||
...POLLING_OPTIONS,
|
||||
});
|
||||
|
||||
wasmWatcher.on('all', () => scheduleWasmWork('wasm', rebuildWasm));
|
||||
|
||||
console.log('[dev] Watching /themes (per-theme reload), the exporter/data files (full regen) and src/**/*.go (wasm rebuild)...');
|
||||
|
||||
const docusaurusBin = join(WEBSITE_DIR, 'node_modules', '.bin', IS_WINDOWS ? 'docusaurus.cmd' : 'docusaurus');
|
||||
const docusaurus = spawn(docusaurusBin, ['start', '--poll', '1000'], {
|
||||
cwd: WEBSITE_DIR,
|
||||
stdio: 'inherit',
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
|
||||
let shuttingDown = false;
|
||||
|
||||
function shutdown(signal) {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
shuttingDown = true;
|
||||
themesWatcher.close();
|
||||
sharedWatcher.close();
|
||||
wasmWatcher.close();
|
||||
docusaurus.kill(signal);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
docusaurus.on('exit', (code, signal) => {
|
||||
themesWatcher.close();
|
||||
sharedWatcher.close();
|
||||
wasmWatcher.close();
|
||||
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.exit(signal ? 1 : code ?? 0);
|
||||
});
|
||||
Reference in New Issue
Block a user