diff --git a/packages/core/src/Page/PageVueServerRenderer.ts b/packages/core/src/Page/PageVueServerRenderer.ts index 1a89289eed..96b4af05ba 100644 --- a/packages/core/src/Page/PageVueServerRenderer.ts +++ b/packages/core/src/Page/PageVueServerRenderer.ts @@ -1,23 +1,20 @@ -/* 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 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'; import { PluginManager } from '../plugins/PluginManager.js'; +const require = createRequire(import.meta.url); + let customElementTagsCache: Set | undefined; let bundle = { ...vueCommonAppFactory }; @@ -91,17 +88,24 @@ 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; +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 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 (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); + } catch (e) { + logger.error(e); + } + + return mod.exports.default ?? mod.exports; } /** @@ -154,7 +158,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 = requireFromString(newBundle); Object.values(pageEntries).forEach(async (pageEntry) => { const { page, renderFn } = pageEntry; @@ -168,4 +172,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({}); + }); + }); });