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

What is the best way to combine the elements of two arrays within a v-for loop?

Within my code, I have defined two arrays: users and projects. Each array contains unique numbers as IDs. A project can be owned by multiple users, so in the projects array, there is an array of user IDs named ownersId that corresponds to the id of users i ...

What is the best way to add a dynamic URL in <router-link> using VueJS?

I am currently utilizing the v-for directive to retrieve URL names. However, I am encountering difficulties in properly passing the value obtained from an instance of v-for as the URL name. <template> <v-list-tile class="pl-5" v-for="choice i ...

Using the `ngrx` library to perform an entity upsert operation with the

I am facing a certain challenge in my code. I have an action defined as follows: export const updateSuccess = createAction('Success', props<{ someId: string }>()); In the reducer, I have an adapter set up like this: export const adapter: ...

Struggling with getting my Vue-CLI app deployed on Heroku

After diligently following all the steps outlined in this tutorial: https://codeburst.io/quick-n-clean-way-to-deploy-vue-webpack-apps-on-heroku-b522d3904bc8 I encountered an error message when checking my app at: The error indicated: Method Not Allowed ...

Are all components in Next.js considered client components by default?

I have created a Next.js app using the app folder and integrated the Next Auth library. To ensure that each page has access to the session, I decided to wrap the entire application in a SessionProvider. However, this led to the necessity of adding the &apo ...

Tips for activating multiple buttons at once

Currently, I am facing a challenge with managing three buttons that are dynamically generated within a div using v-for in Vue.js. My goal is to set an active class on the clicked button while removing the active class from the previously selected button. A ...

Code in JavaScript using VueJS to determine if an array includes an object with a certain value in one of its elements

I have a scenario where I need to address the following issue: I have an Object with a property called specs. This property consists of an Array that contains several other Objects, each having two properties: name value Here is an example of what my o ...

Guide to configuring a not null property in Typescript Sequelize:

Hello there! I am trying to figure out how to set a not null property using TypeScript Sequelize. I have tried using the @NotNull decorator, but unfortunately it does not seem to be working. The errors I am encountering are as follows: Validation error: W ...

We are encountering an issue in Node.js with Mongoose where an instance of TypeError is being received instead of the required string type for the "path" argument

An Error Occurred Connection Successful, Port 4000 Listing Form node:internal/errors:464 ErrorCaptureStackTrace(err); ^ TypeError [ERR_INVALID_ARG_TYPE]: The argument for "path" must be a string. Instead, received a TypeError instance. at new NodeError ...

Are fp-ts and Jest the perfect pairing for testing Option and Either types with ease?

When working with fp-ts, and conducting unit tests using Jest, I often come across scenarios where I need to test nullable results, typically represented by Option or Either (usually in array find operations). What is the most efficient way to ensure that ...

Avoiding Access-Control-Allow-Origin cors issue when integrating with Stripe

Currently, I am working on setting up a payment system using Stripe. Below is the post request code I am using with Express: try { const session = await stripe.checkout.sessions.create({ line_items: [ { price: `${priceId}`, // M ...

Is Vue instance created if element is present?

I am currently developing an application where some pages contain Vue instances, while others do not. My issue arises when switching from one page to another, and the following warning message appears: [Vue warn]: Cannot find element: #element-id How can ...

`Unexpected behavior when using res.redirect()

Both of these code snippets lead to a redirection to the products/:id path. However, there is a difference in how I need to set up the routes. In the post route, I must include products/, but in the put route, I do not need to include it. Does anyone hav ...

New development: In Express.js, the req.body appears to be empty and req.body.name is showing up as undefined

Something seems off with my code as I am unable to retrieve any data from either req.body or req.body.name. My goal is to extract text from an input field in a React component. Here's the excerpt of my POST request: //posting notes to backend and ...

How can I continuously retrieve data for server components with Urql GraphQL whenever a request is made?

My server component is located at /app/account/page.tsx: const AccountPage = async () => { const username = await getViewer(); return <AccountView username={username} />; }; I am facing an issue where getViewer is not executing on every ...

Is it possible to incorporate underscore.js with express4?

Is it possible to utilize express4 with underscore.js, specifically for printing the variable "name" in index.ejs? Are there any alternative methods within underscore.js that work well with node.js and Express4 capabilities? Note: underscore.js and Expre ...

Enhancing Angular2 Routing with Angular4 UrlSerializer for Seamless HATEOAS Link Integration

As a newcomer to Angular4, I am currently exploring how to consume a HATEOAS API. My goal is to either pass an object that contains the self reference or the self reference link itself through the routing mechanism (for example, by clicking on an edit link ...

Fix a typing issue with TypeScript on a coding assistant

I'm struggling with typing a helper function. I need to replace null values in an object with empty strings while preserving the key-value relationships in typescript // from { name: string | undefined url: string | null | undefined icon: ...

Guidelines for Organizing Angular Interface Files and Implementing Custom Type Guards

In my Angular 2 project, I am utilizing Interfaces and have implemented User Defined Type Guards: grid-metadata.ts export interface GridMetadata { activity: string; createdAt: object; totalReps: number; updatedAt: object; } grid.service.ts ... ...

What categories do input events fall into within Vue?

What Typescript types should be used for input events in Vue to avoid missing target value, key, or files properties when using Event? For example: <input @input="(e: MISSING_TYPE) => {}" /> <input @keypress="(e: MISSING_TYPE) = ...