From 827a706cdeab13c69bda1d800b6771b46a9feaee Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 23 Mar 2026 18:33:48 +0800 Subject: [PATCH 1/6] Fix `markbind serve -d` Markbind hot reload breaks due to the old import syntax used in updating the Markbind Vue Bundle. Change to ESM-compatible method of writing src to a temporary .cjs file that is imported using the file path. ESM only supports similar require functionality when using its experimental vm.Module object that can only be accessed when ran with a flag. --- .gitignore | 2 + .../core/src/Page/PageVueServerRenderer.ts | 43 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index d351437352..4f9a54010e 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,8 @@ packages/cli/test/functional/test_site_templates/test_project/expected/diagrams/ dangerfile.js # --- packages/core --- +# Generate vue-module in case it doesn't get cleaned up (PageVueServerRenderer) +**/vue-module*.cjs # Ignore JS files that are compiled from TS packages/core/src/**/constants.js diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index 1a89289eed..9060717710 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -1,12 +1,9 @@ -/* eslint-disable import/no-import-module-exports */ -/* - Note: the function `requireFromString` causes eslint to detect a false-positive - due to the usage of `module`. Hence, the use of the above `eslint-disable`. -*/ import * as Vue from 'vue'; import { createSSRApp } from 'vue'; import { renderToString } from 'vue/server-renderer'; import { compileTemplate } from 'vue/compiler-sfc'; +import { fileURLToPath } from 'url'; +import { randomUUID } from 'crypto'; import type { SFCTemplateCompileOptions, CompilerOptions } from 'vue/compiler-sfc'; import path from 'path'; @@ -91,17 +88,29 @@ async function compileVuePageCreateAndReturnScript( return outputContent; } -/** - * Referenced from stackOverflow: - * https://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory - * - * Credits to Dominic - */ -function requireFromString(src: string, filename: string) { - const m = new (module.constructor as any)(); - m.paths = module.paths; // without this, we won't be able to require Vue in the string module - m._compile(src, filename); - return m.exports; +async function requireFromString(src: string) { + // Create temporary file to write vue bundle to, as ESM does not fully + // support importing from strings without --experimental-vm-modules flag + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const tmpFile = path.join(__dirname, `vue-module-${randomUUID()}.cjs`); + + try { + await fs.writeFile(tmpFile, src); + } catch (e) { + logger.error(e); + throw e; + } + logger.verbose(`Wrote vue-module bundle to: ${tmpFile}`); + + try { + const module = await import(tmpFile); + return module.default; + } catch (e) { + logger.error(e); + throw e; + } finally { + await fs.unlink(tmpFile); + } } /** @@ -154,7 +163,7 @@ async function updateMarkBindVueBundle(newBundle: string): Promise { Bundle is regenerated by webpack and built pages are re-rendered with the latest bundle.`); // reassign the latest updated MarkBindVue bundle - bundle = requireFromString(newBundle, ''); + bundle = await requireFromString(newBundle); Object.values(pageEntries).forEach(async (pageEntry) => { const { page, renderFn } = pageEntry; From 37d604ec9ac810302de611d9917e149573fbfa05 Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 23 Mar 2026 18:50:58 +0800 Subject: [PATCH 2/6] Fix typo in gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f9a54010e..2adb7d09f0 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ packages/cli/test/functional/test_site_templates/test_project/expected/diagrams/ dangerfile.js # --- packages/core --- -# Generate vue-module in case it doesn't get cleaned up (PageVueServerRenderer) +# Generated vue-module in case it doesn't get cleaned up (PageVueServerRenderer) **/vue-module*.cjs # Ignore JS files that are compiled from TS From e10b76f7b472ab249e1b9811a0b80cb7193cdc4b Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 23 Mar 2026 19:40:05 +0800 Subject: [PATCH 3/6] Use createRequire workaround to bundle vue Implementation currently writes to a temp file, which is placed in the directory of the source code so that it can crawl the require tree. This may not work in production environments, especially if markbind-cli was installed globally as a root user and used as a normal user, as it wouldn't be able to write into the directory that markbind lives in. (additionally, we can't write anywhere else as it then wouldn't be able to crawl the dep tree) Use createRequire with an eval workaround to import it instead. This prevents the need of writing to a temp directory, similar to before. However, ESM still does not expose the `module` object, so the same method as before wouldn't work. Therefore, use Function() to import it, passing in the constructed require object. --- .../core/src/Page/PageVueServerRenderer.ts | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index 9060717710..d565e95256 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -2,14 +2,12 @@ import * as Vue from 'vue'; import { createSSRApp } from 'vue'; import { renderToString } from 'vue/server-renderer'; import { compileTemplate } from 'vue/compiler-sfc'; -import { fileURLToPath } from 'url'; -import { randomUUID } from 'crypto'; import type { SFCTemplateCompileOptions, CompilerOptions } from 'vue/compiler-sfc'; +import { createRequire } from 'module'; import path from 'path'; import fs from 'fs-extra'; import vueCommonAppFactory from '@markbind/core-web/dist/js/vueCommonAppFactory.min.js'; - import * as logger from '../utils/logger.js'; import type { PageConfig, PageAssets } from './PageConfig.js'; import type { Page } from './index.js'; @@ -88,29 +86,21 @@ async function compileVuePageCreateAndReturnScript( return outputContent; } -async function requireFromString(src: string) { - // Create temporary file to write vue bundle to, as ESM does not fully - // support importing from strings without --experimental-vm-modules flag - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const tmpFile = path.join(__dirname, `vue-module-${randomUUID()}.cjs`); - - try { - await fs.writeFile(tmpFile, src); - } catch (e) { - logger.error(e); - throw e; - } - logger.verbose(`Wrote vue-module bundle to: ${tmpFile}`); - - try { - const module = await import(tmpFile); - return module.default; - } catch (e) { - logger.error(e); - throw e; - } finally { - await fs.unlink(tmpFile); - } +function requireFromString(src: string) { + // Use createRequire since bundle is CJS. This allows require() calls within the bundle + // to be resolved relative to this file. + const require = createRequire(import.meta.url); + const mod = { exports: {} as any }; + + // Use Function (like eval) to load bundle in global scope for usage + // How this works: It passes in require from createRequire, the module and exports + // object into the `src` code as parameters. The `src` code then uses these naturally + // and populates the mod object, while using the require() from createRequire to + // load dependencies. + // eslint-disable-next-line @typescript-eslint/no-implied-eval + new Function('require', 'module', 'exports', src)(require, mod, mod.exports); + + return mod.exports.default ?? mod.exports; } /** @@ -163,7 +153,7 @@ async function updateMarkBindVueBundle(newBundle: string): Promise { Bundle is regenerated by webpack and built pages are re-rendered with the latest bundle.`); // reassign the latest updated MarkBindVue bundle - bundle = await requireFromString(newBundle); + bundle = requireFromString(newBundle); Object.values(pageEntries).forEach(async (pageEntry) => { const { page, renderFn } = pageEntry; From 1e6caf1ca3986801a050d208bd13b7d37195031d Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 23 Mar 2026 19:59:58 +0800 Subject: [PATCH 4/6] Remove outdated ignore rule --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2adb7d09f0..d351437352 100644 --- a/.gitignore +++ b/.gitignore @@ -86,8 +86,6 @@ packages/cli/test/functional/test_site_templates/test_project/expected/diagrams/ dangerfile.js # --- packages/core --- -# Generated vue-module in case it doesn't get cleaned up (PageVueServerRenderer) -**/vue-module*.cjs # Ignore JS files that are compiled from TS packages/core/src/**/constants.js From d0130c05538d66d2621d193b4c03648c25281fbf Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Fri, 27 Mar 2026 13:51:17 +0800 Subject: [PATCH 5/6] Add test cases for requireFromString requireFromString is untested and private Due to changes in build output, this method broke silently, highlighting importance of testing this section. Add unit tests checking import functionality. --- .../core/src/Page/PageVueServerRenderer.ts | 9 ++- .../unit/Page/PageVueServerRenderer.test.ts | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index d565e95256..f8d3183604 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -97,8 +97,12 @@ function requireFromString(src: string) { // object into the `src` code as parameters. The `src` code then uses these naturally // and populates the mod object, while using the require() from createRequire to // load dependencies. - // eslint-disable-next-line @typescript-eslint/no-implied-eval - new Function('require', 'module', 'exports', src)(require, mod, mod.exports); + try { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + new Function('require', 'module', 'exports', src)(require, mod, mod.exports); + } catch (e) { + logger.error(e); + } return mod.exports.default ?? mod.exports; } @@ -167,4 +171,5 @@ export const pageVueServerRenderer = { renderVuePage, updateMarkBindVueBundle, savePageRenderFnForHotReload, + requireFromString, }; diff --git a/packages/core/test/unit/Page/PageVueServerRenderer.test.ts b/packages/core/test/unit/Page/PageVueServerRenderer.test.ts index c045ad4dbf..f5c26ea88a 100644 --- a/packages/core/test/unit/Page/PageVueServerRenderer.test.ts +++ b/packages/core/test/unit/Page/PageVueServerRenderer.test.ts @@ -89,4 +89,72 @@ describe('PageVueServerRenderer', () => { expect(isCustomElement('any-tag')).toBe(false); }); }); + + describe('requireFromStringMethod', () => { + test('imports CJS javascript code', () => { + const src = ` + require('node:fs'); + + function helloWorld() { + return 'Hello World!'; + } + module.exports = { helloWorld }; + `; + + const module = pageVueServerRenderer.requireFromString(src); + + // Assert that helloWorld method is present in module object + expect('helloWorld' in module).toBe(true); + }); + + test('imports a mock Vue bundle', () => { + const mockVueBundle = ` + const Vue = require('vue'); + + const MarkBindVue = { + plugin: { + install: function(app) { + app.config.globalProperties.$test = 'test'; + } + } + }; + + const appFactory = function() { + return { + data() { + return { test: 'value' }; + } + }; + }; + + module.exports = { MarkBindVue, appFactory }; + `; + + const module = pageVueServerRenderer.requireFromString(mockVueBundle); + const appFactory = module.appFactory(); + + // Assert that bundle contains expected properties + expect(module).toHaveProperty('MarkBindVue'); + expect(module).toHaveProperty('appFactory'); + expect(typeof module.MarkBindVue.plugin.install).toBe('function'); + expect(typeof module.appFactory).toBe('function'); + expect(appFactory).toBeDefined(); + }); + + test('executes gracefully with invalid code', () => { + const invalidSrc = ` + (defun hello-world () + (format t "hello world")) + + (hello-world) + `; + + // Act: Invalid src should execute and not throw exceptions + const module = pageVueServerRenderer.requireFromString(invalidSrc); + + // Assert that module has no exports + // (logger should inform of error) + expect(module).toEqual({}); + }); + }); }); From 5cb16854207d8db889fc0190a9bd2fdd15e93b5c Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Fri, 27 Mar 2026 14:19:45 +0800 Subject: [PATCH 6/6] Move require creation to module scope --- packages/core/src/Page/PageVueServerRenderer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index f8d3183604..96b4af05ba 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -13,6 +13,8 @@ import type { PageConfig, PageAssets } from './PageConfig.js'; import type { Page } from './index.js'; import { PluginManager } from '../plugins/PluginManager.js'; +const require = createRequire(import.meta.url); + let customElementTagsCache: Set | undefined; let bundle = { ...vueCommonAppFactory }; @@ -89,14 +91,13 @@ async function compileVuePageCreateAndReturnScript( function requireFromString(src: string) { // Use createRequire since bundle is CJS. This allows require() calls within the bundle // to be resolved relative to this file. - const require = createRequire(import.meta.url); const mod = { exports: {} as any }; // Use Function (like eval) to load bundle in global scope for usage // How this works: It passes in require from createRequire, the module and exports // object into the `src` code as parameters. The `src` code then uses these naturally // and populates the mod object, while using the require() from createRequire to - // load dependencies. + // load dependencies (which are CJS `require` calls themselves). try { // eslint-disable-next-line @typescript-eslint/no-implied-eval new Function('require', 'module', 'exports', src)(require, mod, mod.exports);