From d7d0aba091b616cc4ee8c89588c82958a4a6f8b8 Mon Sep 17 00:00:00 2001 From: Aetf Date: Fri, 21 Aug 2026 13:25:04 -0700 Subject: [PATCH 1/2] Change: convert hexo scripts to ESM Set "type": "module" and move the real logic of all four hexo scripts to ESM modules under lib/, taking the hexo instance as an explicit parameter instead of the vm-injected global. Hexo still runs files under scripts/ as CJS text in a vm sandbox where ESM syntax and dynamic import() are unavailable (hexojs/hexo#5525), so each script is now a one-line CJS stub going through lib/esm-bridge.cjs, which lives outside the vm and can import() the real module. The stubs and the bridge can be deleted once hexo loads ESM scripts natively (hexojs/hexo#5820). Rendered output is unchanged (verified: data-zoom-src rewriting, post_link anchors, xkcd_infos payload, open-sidebar links all present in the generated site; no script load errors). Co-Authored-By: Claude Fable 5 --- lib/esm-bridge.cjs | 14 +++++++++ lib/filters/after_post_render.js | 39 ++++++++++++++++++++++++ lib/tags/open_sidebar.js | 23 ++++++++++++++ lib/tags/post_link.js | 39 ++++++++++++++++++++++++ lib/tags/random_xkcd.js | 40 +++++++++++++++++++++++++ package.json | 1 + scripts/filters/after_post_render.js | 45 ++-------------------------- scripts/tags/open_sidebar.js | 24 ++------------- scripts/tags/post_link.js | 39 ++---------------------- scripts/tags/random_xkcd.js | 39 ++---------------------- 10 files changed, 164 insertions(+), 139 deletions(-) create mode 100644 lib/esm-bridge.cjs create mode 100644 lib/filters/after_post_render.js create mode 100644 lib/tags/open_sidebar.js create mode 100644 lib/tags/post_link.js create mode 100644 lib/tags/random_xkcd.js diff --git a/lib/esm-bridge.cjs b/lib/esm-bridge.cjs new file mode 100644 index 0000000..d9baa7b --- /dev/null +++ b/lib/esm-bridge.cjs @@ -0,0 +1,14 @@ +'use strict'; + +const pathFn = require('path'); +const { pathToFileURL } = require('url'); + +// Hexo runs files under scripts/ as CJS text in a vm sandbox, where ESM syntax +// and direct dynamic import() are unavailable (hexojs/hexo#5525). Each script is +// therefore a one-line CJS stub calling this bridge, which lives outside the vm +// and can import() the real ESM module from lib/. Delete the stubs and this file +// once hexo loads ESM scripts natively (hexojs/hexo#5820). +exports.load = function load(hexo, relToLib) { + const url = pathToFileURL(pathFn.join(__dirname, relToLib)); + return import(url.href).then(m => m.default(hexo)); +}; diff --git a/lib/filters/after_post_render.js b/lib/filters/after_post_render.js new file mode 100644 index 0000000..89e2870 --- /dev/null +++ b/lib/filters/after_post_render.js @@ -0,0 +1,39 @@ +import pathFn from 'path'; + +export default function register(hexo) { + function image_version(oldPath, { thumbProfile = 'body', hugeProfile = 'huge' } = {}) { + const dir = pathFn.dirname(oldPath); + let base = pathFn.basename(oldPath); + + // if base already starts with a prefix + for (const key in hexo.config.responsive_images.sizes) { + if (base.startsWith(key + '_')) { + thumbProfile = key; + base = base.slice(key.length + 1); + break; + } + } + + if (dir === '.') { + return { + thumb: thumbProfile + '_' + base, + huge: hugeProfile + '_' + base, + }; + } + return { + thumb: dir + '/' + thumbProfile + '_' + base, + huge: dir + '/' + hugeProfile + '_' + base, + }; + } + + function mediumZoomFilter(post) { + post.content = post.content.replace(/(]*?) src="([^"]+)"/img, (match, p1, p2) => { + hexo.log.info('Responsive image', p2); + const { thumb, huge } = image_version(p2); + return `${p1} data-zoom-src="${huge}" src="${thumb}"`; + }); + } + + // the priority must before the next theme's img lazy load filter's 0 + hexo.extend.filter.register('after_post_render', mediumZoomFilter, -10); +} diff --git a/lib/tags/open_sidebar.js b/lib/tags/open_sidebar.js new file mode 100644 index 0000000..2df16c2 --- /dev/null +++ b/lib/tags/open_sidebar.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; + +/** + * A link to open sidebar. Basically only adds open-sidebar class to the tag. + * + * Syntax: + * {% open_sidebar text [, title] %} + */ +export default function register(hexo) { + function openSidebarTag(args) { + var [text, title] = args.join(' ').split(','); + if (!text) return; + + if (title) { + title = _.trim(title); + title = `title="${title}"`; + } + // the href has to be a fragment, so it's pjax safe + return `${text}`; + } + + hexo.extend.tag.register('open_sidebar', openSidebarTag, { ends: false }); +} diff --git a/lib/tags/post_link.js b/lib/tags/post_link.js new file mode 100644 index 0000000..3059fbd --- /dev/null +++ b/lib/tags/post_link.js @@ -0,0 +1,39 @@ +import { htmlTag } from 'hexo-util'; + +/** + * Post link tag + * + * Syntax: + * {% post_link slug[#fragment] [title] %} + */ +export default function register(hexo) { + const Post = hexo.model('Post'); + + function postLinkTag(args) { + let slug = args.shift(); + + if (!slug) return; + + let frag = ''; + [slug, frag] = slug.split('#'); + if (!frag) { + frag = ''; + } + + if (frag.length > 0) { + frag = '#' + frag; + } + + let post = Post.findOne({ slug }); + if (!post) return; + + let title = args.length ? args.join(' ') : post.title; + + return htmlTag('a', { + href: hexo.config.root + post.path + frag, + title: title + }, title); + } + + hexo.extend.tag.register('post_link', postLinkTag, { ends: false }); +} diff --git a/lib/tags/random_xkcd.js b/lib/tags/random_xkcd.js new file mode 100644 index 0000000..e955f37 --- /dev/null +++ b/lib/tags/random_xkcd.js @@ -0,0 +1,40 @@ +import util from 'util'; +import crypto from 'crypto'; + +import xkcdApi from 'xkcd-api'; + +const xkcd_get = util.promisify(xkcdApi.get); + +export default function register(hexo) { + async function randomXkcd() { + // Hexo >= 7 invokes tags with the nunjucks render context as `this`, which has no + // `site`. Go through hexo's locals instead, which works on every version. + const comics = hexo.locals.get('data').xkcd || []; + const infos = await Promise.all(comics.map(c => xkcd_get(c))); + // build a JSON list with less info to include in the page + const list = infos.map(el => ({ + num: el.num, + alt: el.alt, + title: el.title, + img: el.img + })); + + const tag_id = crypto.randomBytes(4).toString('hex'); + return ` + `; + } + + hexo.extend.tag.register('random_xkcd', randomXkcd, { ends: false, async: true }); +} diff --git a/package.json b/package.json index fc08830..80292e1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "hexo-site", "version": "0.0.0", "private": true, + "type": "module", "hexo": { "version": "8.1.2" }, diff --git a/scripts/filters/after_post_render.js b/scripts/filters/after_post_render.js index e19d57a..e413dd0 100644 --- a/scripts/filters/after_post_render.js +++ b/scripts/filters/after_post_render.js @@ -1,44 +1,3 @@ 'use strict'; - -const pathFn = require('path'); - -function image_version(oldPath, { thumbProfile = 'body', hugeProfile = 'huge' } = {}) { - const dir = pathFn.dirname(oldPath); - let base = pathFn.basename(oldPath); - - // if base already starts with a prefix - for (const key in hexo.config.responsive_images.sizes) { - if (base.startsWith(key + '_')) { - thumbProfile = key; - base = base.slice(key.length + 1); - break; - } - } - - if (dir === '.') { - return { - thumb: thumbProfile + '_' + base, - huge: hugeProfile + '_' + base, - }; - } - return { - thumb: dir + '/' + thumbProfile + '_' + base, - huge: dir + '/' + hugeProfile + '_' + base, - }; -} - -function mediumZoomFilter(post) { - const content = post.content; - post.content = post.content.replace(/(]*?) src="([^"]+)"/img, (match, p1, p2) => { - hexo.log.info('Responsive image', p2); - const { thumb, huge } = image_version(p2); - return `${p1} data-zoom-src="${huge}" src="${thumb}"`; - }); - - if (content.includes('img')) { - // console.log('after render', content); - } -} - -// the priority must before the next theme's img lazy load filter's 0 -hexo.extend.filter.register('after_post_render', mediumZoomFilter, -10); +// CJS stub: hexo runs scripts/ files as CJS in a vm; real logic is ESM in lib/. See lib/esm-bridge.cjs. +await require('../../lib/esm-bridge.cjs').load(hexo, 'filters/after_post_render.js'); diff --git a/scripts/tags/open_sidebar.js b/scripts/tags/open_sidebar.js index 541d8e2..179aa5e 100644 --- a/scripts/tags/open_sidebar.js +++ b/scripts/tags/open_sidebar.js @@ -1,23 +1,3 @@ 'use strict'; - -const _ = require('lodash'); - -/** - * A link to open sidebar. Basically only adds open-sidebar class to the tag. - * - * Syntax: - * {% open_sidebar text [, title] %} - */ -function openSidebarTag(args) { - var [text, title] = args.join(' ').split(','); - if (!text) return; - - if (title) { - title = _.trim(title); - title = `title="${title}"`; - } - // the href has to be a fragment, so it's pjax safe - return `${text}`; -}; - -hexo.extend.tag.register('open_sidebar', openSidebarTag, { ends: false }); +// CJS stub: hexo runs scripts/ files as CJS in a vm; real logic is ESM in lib/. See lib/esm-bridge.cjs. +await require('../../lib/esm-bridge.cjs').load(hexo, 'tags/open_sidebar.js'); diff --git a/scripts/tags/post_link.js b/scripts/tags/post_link.js index 35bf98c..a9e446b 100644 --- a/scripts/tags/post_link.js +++ b/scripts/tags/post_link.js @@ -1,38 +1,3 @@ 'use strict'; - -const { htmlTag } = require('hexo-util'); -const Post = hexo.model('Post'); - -/** - * Post link tag - * - * Syntax: - * {% post_link slug[#fragment] [title] %} - */ -function postLinkTag(args) { - let slug = args.shift(); - - if (!slug) return; - - let frag = ''; - [slug, frag] = slug.split('#'); - if (!frag) { - frag = ''; - } - - if (frag.length > 0) { - frag = '#' + frag; - } - - let post = Post.findOne({ slug }); - if (!post) return; - - let title = args.length ? args.join(' ') : post.title; - - return htmlTag('a', { - href: hexo.config.root + post.path + frag, - title: title - }, title); -}; - -hexo.extend.tag.register('post_link', postLinkTag, { ends: false }); \ No newline at end of file +// CJS stub: hexo runs scripts/ files as CJS in a vm; real logic is ESM in lib/. See lib/esm-bridge.cjs. +await require('../../lib/esm-bridge.cjs').load(hexo, 'tags/post_link.js'); diff --git a/scripts/tags/random_xkcd.js b/scripts/tags/random_xkcd.js index 5ff9be7..484f103 100644 --- a/scripts/tags/random_xkcd.js +++ b/scripts/tags/random_xkcd.js @@ -1,38 +1,3 @@ 'use strict'; - -const util = require('util'); -const crypto = require('crypto'); - -const xkcd_get = util.promisify(require('xkcd-api').get); - -async function randomXkcd() { - // Hexo >= 7 invokes tags with the nunjucks render context as `this`, which has no - // `site`. Go through hexo's locals instead, which works on every version. - const comics = hexo.locals.get('data').xkcd || []; - const infos = await Promise.all(comics.map(c => xkcd_get(c))); - // build a JSON list with less info to include in the page - const list = infos.map(el => ({ - num: el.num, - alt: el.alt, - title: el.title, - img: el.img - })); - - const tag_id = crypto.randomBytes(4).toString('hex'); - return ` - `; -} - -hexo.extend.tag.register('random_xkcd', randomXkcd, { ends: false, async: true }); +// CJS stub: hexo runs scripts/ files as CJS in a vm; real logic is ESM in lib/. See lib/esm-bridge.cjs. +await require('../../lib/esm-bridge.cjs').load(hexo, 'tags/random_xkcd.js'); From f2c5c38bc114a4399fa3c5b831abae13612dddd5 Mon Sep 17 00:00:00 2001 From: Aetf Date: Fri, 21 Aug 2026 13:25:11 -0700 Subject: [PATCH 2/2] Change: convert ava tests to ESM Import syntax throughout; tests/helpers uses fileURLToPath(import.meta.url) in place of __dirname and keeps loading hexo lazily via dynamic import so tests not using it don't pay for it. Directory import './helpers' becomes the explicit './helpers/index.js' as ESM requires. All 43 tests pass with no snapshot changes. Co-Authored-By: Claude Fable 5 --- tests/helpers/index.js | 25 ++++++++++++------------- tests/math-rendering.js | 8 +++++--- tests/ocs_site_verification.js | 5 +++-- tests/post.js | 5 +++-- tests/prism-rendering.js | 5 +++-- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/tests/helpers/index.js b/tests/helpers/index.js index b226dd0..dbf5741 100644 --- a/tests/helpers/index.js +++ b/tests/helpers/index.js @@ -1,7 +1,10 @@ -const fs = require('fs/promises'); -const pathFn = require('path'); -const { JSDOM } = require('jsdom'); +import fs from 'fs/promises'; +import pathFn from 'path'; +import { fileURLToPath } from 'url'; +import { JSDOM } from 'jsdom'; + +const __dirname = pathFn.dirname(fileURLToPath(import.meta.url)); const PUBLIC_DIR = pathFn.resolve(__dirname, "..", "..", "public"); async function recursiveRoutes(basedir, prefixUrl) { @@ -18,27 +21,23 @@ async function recursiveRoutes(basedir, prefixUrl) { return fileRoutes.concat(subRoutes); } -async function listRoutes() { +export async function listRoutes() { return await recursiveRoutes(pathFn.join(PUBLIC_DIR, 'blog'), '/'); } -async function getRoute(path) { +export async function getRoute(path) { return await JSDOM.fromFile(pathFn.join(PUBLIC_DIR, path)); } -async function getRouteFile(path) { +export async function getRouteFile(path) { return await fs.readFile(pathFn.join(PUBLIC_DIR, path), { encoding: 'utf-8' }); } -async function getHexo(level) { - const Hexo = require('hexo'); +export async function getHexo(level) { + // import lazily so tests not using hexo don't pay for loading it + const { default: Hexo } = await import('hexo'); const hexo = new Hexo(); hexo.log.level = level || 40 // WARN; await hexo.init(); return hexo; } - -module.exports.getRoute = getRoute; -module.exports.getRouteFile = getRouteFile; -module.exports.getHexo = getHexo; -module.exports.listRoutes = listRoutes; diff --git a/tests/math-rendering.js b/tests/math-rendering.js index ce130ff..1a1ea2f 100644 --- a/tests/math-rendering.js +++ b/tests/math-rendering.js @@ -1,6 +1,8 @@ -const test = require('ava'); -const fs = require('fs/promises'); -const { getHexo } = require('./helpers'); +import fs from 'fs/promises'; + +import test from 'ava'; + +import { getHexo } from './helpers/index.js'; const saveHtml = false; diff --git a/tests/ocs_site_verification.js b/tests/ocs_site_verification.js index 6315ab7..9341136 100644 --- a/tests/ocs_site_verification.js +++ b/tests/ocs_site_verification.js @@ -1,5 +1,6 @@ -const test = require('ava'); -const { getRoute } = require('./helpers'); +import test from 'ava'; + +import { getRoute } from './helpers/index.js'; test('home page contains OCS site verification', async t => { const dom = await getRoute('index.html'); diff --git a/tests/post.js b/tests/post.js index c64eddd..45f0584 100644 --- a/tests/post.js +++ b/tests/post.js @@ -1,5 +1,6 @@ -const test = require('ava'); -const { getRoute, listRoutes } = require('./helpers'); +import test from 'ava'; + +import { getRoute, listRoutes } from './helpers/index.js'; test('has tags in header', async t => { const dom = await getRoute('blog/2016/08/20/gsoc-kdevelop-lldb-final-report/index.html'); diff --git a/tests/prism-rendering.js b/tests/prism-rendering.js index 8eb77c4..bba4aec 100644 --- a/tests/prism-rendering.js +++ b/tests/prism-rendering.js @@ -1,5 +1,6 @@ -const test = require('ava'); -const { getRouteFile } = require('./helpers'); +import test from 'ava'; + +import { getRouteFile } from './helpers/index.js'; test('prism bundle contains language', async t => { const js = await getRouteFile('assets/prism-bundle.js');