API endpoint generating a Vue component as a rendered output

In the process of developing a document templater service, I am faced with the challenge of handling numerous document templates (contracts, protocols, etc.) written in Vue. The concept revolves around clients sending props in the body, which are then passed to the Vue component (document template). Subsequently, the rendered document is sent back to the client as a response. Vue proves to be an ideal fit for this task due to its user-friendly nature and flexibility required for handling complex templates.

However, the complexity arises from the varied script parts in each type of document. This makes it impossible to simply extract the template part of the component and render it with context. Instead, I need to transpile all sections of my Vue template (script setup + TypeScript, template, and CSS) before specifying the context (props) for my component.

To achieve the transpilation process, I am experimenting with webpack (vue-loader, ts-loader) using a commonjs configuration. Utilizing ESM would require a substantial refactor of my Express app, which is currently quite large.

The issue lies in importing the bundle produced by webpack. Whenever I attempt to import it with the following code snippet:

export async function loadSSRTemplate(templateName: string) {
  // Although I use commonjs, this section operates on a typescript layer that is not yet compiled
  // @ts-ignore
  const bundle = await import('@/ssr/dist/main.js');
  console.log(bundle.default); //this always returns undefined
  return bundle[templateName]; //similarly, this doesn't work as intended
}

I struggle to find a method to import my transpiled Vue components from the bundle so they can seamlessly integrate with createSSRApp and the h() method within my templater service.

While I understand that the problem may not be apparent without delving into the complete configuration and structure of Vue components, I seek guidance from those who have encountered similar scenarios in the past. Is this feasible or am I investing my efforts in vain?

PS: If there are alternative technologies that could address this challenge effectively, feel free to suggest them. Nonetheless, if possible, I prefer performing the transpilation beforehand via build processes for enhanced performance.

My tech stack: Templates: Vue 3 + TypeScript + Composition API (setup script) + Webpack 5.9

Service: Node 18 (Express) + TypeScript. Dockerized using Node:18. Execution through ts-node-dev with commonjs config

EDIT: For those interested in further details, here is my webpack configuration:

const path = require('path');
const nodeExternals = require('webpack-node-externals');
const { VueLoaderPlugin } = require('vue-loader/dist/index');

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './webpack-entry-point.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    libraryTarget: 'commonjs2',
  },
  externals: nodeExternals({
    allowlist: /\.css$/,
  }),
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: { appendTsSuffixTo: [/\.vue$/] },
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
    ],
  },
  resolve: {
    alias: {
      '@templates': path.resolve(__dirname, './templates'),
    },
    extensions: ['.ts', '.js', '.vue', '.json'],
  },
  plugins: [new VueLoaderPlugin()],
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
};

The entry point comprises a simple JavaScript file that imports all top-level components and exports them, for example:

import NajomnaZmluva from '@templates/reality_module/NajomnaZmluva.vue';

export { NajomnaZmluva };

Here is a glimpse of the render method implementation in my templater service:

async render(id: string, data: any): Promise<string> {
  if (!process.env.SSR_DOCUMENT_TEMPLATE_PATH) {
    console.error('SSR_DOCUMENT_TEMPLATE_PATH is not defined');
    throw new UnknownServerError();
  }

  const documentTemplate = await this.retrieve(id);

  if (!documentTemplate.schema) {
    throw new BadRequestError(
      responsesConstants.errors.badRequestError.schemaNotDefined,
    );
  }

  this.validateSchema(
    documentTemplate.schema as Record<string, schemaType>,
    data,
  );

  //The template name will be dynamically specified; the hardcoded value here serves testing purposes only
  const Template = await loadSSRTemplate('NajomnaZmluva');

  //I cannot determine if the code below functions correctly since I am stuck at loading the bundle

  const ssrDocument = createSSRApp({
    template: h(Template, { props: data }),
  });

  return await renderToString(ssrDocument);
}

EDIT: Progress is being made. I will share the solution in a few hours for those eager to delve deeper into this topic.

Answer №1

If you're looking for a solution and my approach to implementing this functionality, I'll keep updating this answer until I have a fully operational endpoint with the described features.

  1. I managed to resolve the issue where webpack was not generating functional bundles that could export transpiled components. This is specifically for commonjs environments, but I believe ESM works just as well. Here's how I modified the output block (refer to the comments):
//webpack.config.js
...

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    //Change from previous config:
    //I removed libraryTarget: 'commonjs2' and added library: { type: 'commonjs-module' }
    library: {
      type: 'commonjs-module',
    },
  },

...

EDIT: Successfully completed the ssr.util.ts file which manages ssr vue rendering from the webpack bundle

// Import necessary functions and types from Vue and Vue server-renderer
import { renderToString } from 'vue/server-renderer';
import { createSSRApp, h, SetupContext } from 'vue';
import { RenderFunction } from '@vue/runtime-dom';

// Define an interface for the structure of a transpiled Vue component
interface TranspiledComponent {
  ssrRender(props: Record<string, unknown>): RenderFunction;
  setup(props: Record<string, unknown>, context: SetupContext): RenderFunction;
}

// Function to load a transpiled component from a webpack bundle
export async function loadSSRTemplate(templateName: string): TranspiledComponent {
  // @ts-ignore is used to bypass TypeScript checks,
  // as the specific module structure is known at runtime
  //You can problably generate types in webpack to handle this but i didn't find it that important
  const bundle = await import('@/ssr/dist/main.js');
  return bundle[templateName];
}

// Function to render a Vue component with provided props into an HTML string
export async function renderVueComponent(
  Component: TranspiledComponent,
  propsData: Record<string, unknown>,
) {
  // Create a Vue app for SSR with the component and its props
  const app = createSSRApp({
    render() {
      // The 'h' function is used to create a VNode for the component
      return h(Component, propsData);
    },
  });

  // Render the app to an HTML string and return it
  return await renderToString(app);
}

This utility functions seamlessly with my webpack configuration detailed in the question, but you need to adjust the output block as per the code snippet in this response. If you utilize my configuration, two files will be generated - main.js and chunk-vendors.js. Your Vue components are located in main.js, hence that's the file you should load in the loadSSRTemplate function. Additional details regarding my relevant tech stack can be found in the original question.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Error: Unable to generate MD5 hash for the file located at 'C:....gradle-bintray-plugin-1.7.3.jar' in Ionic framework

When attempting to use the command ionic cordova run android, an error occurred that prevented the successful execution: The process failed due to inability to create an MD5 hash for a specific file in the specified directory. This issue arose despite suc ...

How to update the selected autocomplete item in Vue using programming techniques?

Although I am still learning Vue, consider the following scenario: <v-autocomplete v-model="defaultUser" :hint="`User: ${defaultUser.username}`" :items="users" :item-text="item =>`${item.firstName} - $ ...

Experiencing a bug where Bootstrap is causing a white screen to appear instead of rendering the page properly

I'm currently grappling with getting bootstrap to function properly in Node.js using the Jade template engine. Whenever I attempt to execute the index.jade file, all I get is a blank white page. div.topbar div.fill div.container a.brand( ...

Error encountered when utilizing the jwt-express library

Recently, I attempted to utilize the JWT-Express package for NodeJS to generate a JWT token for user login purposes. Unfortunately, I encountered a TypeError while working with the package. This is the snippet of my code: var express = require('expr ...

Locate broken image links in Vue project

I have a Vue 3 project where all images are stored in public/assets/image and referenced in .vue files as assets/image/image.png. Occasionally, I may enter incorrect paths or update paths that are no longer valid. Is there an automated way to detect and ...

How can I set the destination in multer to upload images to a folder on a remote server in a node.js application?

app.post('/photo',[ multer({ dest:'http://example.com/images/destination/',limits: {files: 8,fields: 18}} I am encountering an issue with this code because I am on a different server and attempting to upload files to a folder on ano ...

What does the `Class<Component>` represent in JavaScript?

Apologies for the lackluster title (I struggled to think of a better one). I'm currently analyzing some Vue code, and I stumbled upon this: export function initMixin (Vue: Class<Component>) { // ... } What exactly does Class<Component> ...

Is there a way in Express.js to handle requests for both JSON and HTML in a single function?

Is there a way to handle requests in express.js using a single function for both html and json data? I am looking for a solution where I can have one route for both /users and /users.json, similar to how Rails handles routes and controllers. This would a ...

Implementing dynamic class bindings with Vue.js using v-for

I am currently using a v-for loop in Vue.js to create a list of items populated with data from an API. Here is an example of the items array: items: [ { foo: 'something', number: 60 }, { foo: 'anything', ...

What could be causing my node server's REST endpoints to not function properly?

Here is a snippet of my index.js file: var http = require('http'); var express = require('express'); var path = require('path'); var bodyParser = require('body-parser') var app = express(); var currentVideo = &apos ...

Enhance the appearance of the activated button through VueJS styling

This is not associated with the router menu. I have a pricing document composed of 3 sub-documents. Above the currently visible document are 3 buttons that, when clicked, display the corresponding document. View the design of the pricing document I con ...

Issues with Pagination in Laravel and Vue

After clicking on page 2 of the pagination component, the application receives data for page=2, but it does not display on the screen. Instead, the pagination reverts to page 1 and everything starts over. I am utilizing bootstrap-vue as my UI component lib ...

Interactive JS chart for visually representing values within a preset range in Vue.js

I was in need of a custom JavaScript chart specifically designed to display a value within a specified upper and lower limit. The main application for this would be illustrating the current stock price between its 52-week high and low ranges. It was essent ...

What is the reason for parent rows not stretching fully across the width of the page?

I am working on creating a simple table in Vue.js with Bootstrap. When the arrow is clicked, the child row is displayed, and I want it to appear below the parent row. I achieved this by setting display:flexbox; flex-direction:column; Here is the HTML tabl ...

Storing dataset characteristics in a JSON file utilizing Vue.js form response

I am currently working on creating a JSON file to store all the answers obtained from a Form. Some of the input fields have an additional dataset attribute (data-tag). When saving the Form, I aim to extract these 'tags' and include them in the JS ...

Babel Alert: Module 'babel-runtime/core-js/json/stringify' Not Found

Recently, I decided to experiment with Vue.js along with Webpack and everything went smoothly. Now, I am attempting to integrate Vue with Brunch, as recommended by Phoenix framework. However, I encountered a perplexing error: Cannot find module 'ba ...

Troubleshoot: Angular5 Service call not functioning properly when called in ngOnInit

Every time I go to the results component, the service inside ngOnInit behaves as expected. However, when I open the side menu, navigate to another page, and then return to the results page, the page fails to render the results. Instead, the ng-template is ...

Node/Express: The $ symbol is failing to recognize the input for PORT 8080

What steps should I follow to run my PORT on 8080? Is it necessary to install any dependencies? const app = require('express')(); const PORT = 8080; app.listen( PORT, () => console.log('It's now running on http://localhost:$ ...

The Node.js application successfully operates on a local environment, however encounters issues when attempting to run on docker resulting in an error message stating "sh

Despite successfully building the docker image, I am facing difficulties getting the container to run. Below is the content of the package.json file: { "name": "linked-versions-viewer", "version": "1.0.0", &quo ...

Should mutators be encapsulated within a class contained in a JS Module for better code organization and maintainability?

In order to maximize functionality of our new product using JavaScript, we have implemented an Authentication module that manages a tokenPromise which is updated upon user logins or token refreshes. It seems imperative to allow for mutation in this process ...