Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions packages/core/src/Page/PageVueServerRenderer.ts
Original file line number Diff line number Diff line change
@@ -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<string> | undefined;

let bundle = { ...vueCommonAppFactory };
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really cool. u surprise me everyday with something new

} catch (e) {
logger.error(e);
}

return mod.exports.default ?? mod.exports;
Comment thread
Harjun751 marked this conversation as resolved.
}

Comment thread
Harjun751 marked this conversation as resolved.
/**
Expand Down Expand Up @@ -154,7 +158,7 @@ async function updateMarkBindVueBundle(newBundle: string): Promise<void> {
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;
Expand All @@ -168,4 +172,5 @@ export const pageVueServerRenderer = {
renderVuePage,
updateMarkBindVueBundle,
savePageRenderFnForHotReload,
requireFromString,
};
68 changes: 68 additions & 0 deletions packages/core/test/unit/Page/PageVueServerRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
});
});
});
Loading