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 ``;
+ }
+
+ 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 ``;
-};
-
-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');
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');