From 277cd9703f80c42083131d0d9639c6b2b0c28cdd Mon Sep 17 00:00:00 2001
From: mog422 <admin@mog422.net>
Date: Fri, 19 Jul 2024 16:29:46 +0900
Subject: [PATCH 1/3] fix: missing register component in ssr (#1887)

---
 package.json |  2 ++
 src/index.ts | 15 +++++++++++++++
 2 files changed, 17 insertions(+)

diff --git a/package.json b/package.json
index b36e30b00..69285a0c0 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
   "packageManager": "pnpm@8.12.0",
   "dependencies": {
     "chalk": "^4.1.0",
+    "hash-sum": "^2.0.0",
     "watchpack": "^2.4.0"
   },
   "peerDependencies": {
@@ -57,6 +58,7 @@
     "@intlify/vue-i18n-loader": "^3.0.0",
     "@types/cssesc": "^3.0.2",
     "@types/estree": "^0.0.45",
+    "@types/hash-sum": "^1.0.2",
     "@types/jest": "^26.0.13",
     "@types/jsdom": "^16.2.13",
     "@types/mini-css-extract-plugin": "^0.9.1",
diff --git a/src/index.ts b/src/index.ts
index 890dc1cb3..44df75cf2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,6 +11,9 @@ import type {
   SFCTemplateCompileOptions,
   SFCScriptCompileOptions,
 } from 'vue/compiler-sfc'
+
+import hashSum from 'hash-sum'
+
 import { selectBlock } from './select'
 import { genHotReloadCode } from './hotReload'
 import { genCSSModulesCode } from './cssModules'
@@ -363,6 +366,18 @@ export default function loader(
         .join(`\n`) + `\n`
   }
 
+  if (isServer) {
+    code += `\nimport { useSSRContext } from 'vue'\n`
+    code += `const _setup = script.setup\n`
+    ;(code += `script.setup = (props, ctx) => {`),
+      (code += `  const ssrContext = useSSRContext()`),
+      (code += `  ;(ssrContext._registeredComponents || (ssrContext._registeredComponents = new Set())).add(${JSON.stringify(
+        hashSum(loaderContext.request)
+      )});`)
+    code += `  return _setup ? _setup(props, ctx) : undefined`
+    code += `}\n`
+  }
+
   // finalize
   if (!propsToAttach.length) {
     code += `\n\nconst __exports__ = script;`

From 1063af66eda5e5d50e63dab316e96181f1044c6b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ivan=20Nikoli=C4=87?= <niksy5@gmail.com>
Date: Tue, 6 Aug 2024 21:40:48 +0200
Subject: [PATCH 2/3] feat: use internal hash function for SSR module id

---
 package.json | 2 --
 src/index.ts | 4 +---
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 69285a0c0..b36e30b00 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,6 @@
   "packageManager": "pnpm@8.12.0",
   "dependencies": {
     "chalk": "^4.1.0",
-    "hash-sum": "^2.0.0",
     "watchpack": "^2.4.0"
   },
   "peerDependencies": {
@@ -58,7 +57,6 @@
     "@intlify/vue-i18n-loader": "^3.0.0",
     "@types/cssesc": "^3.0.2",
     "@types/estree": "^0.0.45",
-    "@types/hash-sum": "^1.0.2",
     "@types/jest": "^26.0.13",
     "@types/jsdom": "^16.2.13",
     "@types/mini-css-extract-plugin": "^0.9.1",
diff --git a/src/index.ts b/src/index.ts
index 44df75cf2..5648adf50 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,8 +12,6 @@ import type {
   SFCScriptCompileOptions,
 } from 'vue/compiler-sfc'
 
-import hashSum from 'hash-sum'
-
 import { selectBlock } from './select'
 import { genHotReloadCode } from './hotReload'
 import { genCSSModulesCode } from './cssModules'
@@ -372,7 +370,7 @@ export default function loader(
     ;(code += `script.setup = (props, ctx) => {`),
       (code += `  const ssrContext = useSSRContext()`),
       (code += `  ;(ssrContext._registeredComponents || (ssrContext._registeredComponents = new Set())).add(${JSON.stringify(
-        hashSum(loaderContext.request)
+        hash(loaderContext.request)
       )});`)
     code += `  return _setup ? _setup(props, ctx) : undefined`
     code += `}\n`

From b7b45376c75de02adc93fcb326326ef72b23b8d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ivan=20Nikoli=C4=87?= <niksy5@gmail.com>
Date: Tue, 6 Aug 2024 09:40:43 +0200
Subject: [PATCH 3/3] feat: add tests for SSR render

---
 test/fixtures/functional-style.vue | 13 +++++
 test/fixtures/ssr-entry.js         | 17 +++++++
 test/fixtures/ssr-style.vue        | 25 ++++++++++
 test/ssr.spec.ts                   | 79 ++++++++++++++++++++++++++++++
 test/utils.ts                      | 76 ++++++++++++++++++++++++++++
 5 files changed, 210 insertions(+)
 create mode 100644 test/fixtures/functional-style.vue
 create mode 100644 test/fixtures/ssr-entry.js
 create mode 100644 test/fixtures/ssr-style.vue
 create mode 100644 test/ssr.spec.ts

diff --git a/test/fixtures/functional-style.vue b/test/fixtures/functional-style.vue
new file mode 100644
index 000000000..a625668b8
--- /dev/null
+++ b/test/fixtures/functional-style.vue
@@ -0,0 +1,13 @@
+<script>
+import {h} from 'vue';
+export default {
+  functional: true,
+  render () {
+    return h('div', { class: 'foo' }, ['functional'])
+  }
+}
+</script>
+
+<style>
+.foo { color: red; }
+</style>
diff --git a/test/fixtures/ssr-entry.js b/test/fixtures/ssr-entry.js
new file mode 100644
index 000000000..19ea19c93
--- /dev/null
+++ b/test/fixtures/ssr-entry.js
@@ -0,0 +1,17 @@
+import { renderToString } from 'vue/server-renderer'
+import { createSSRApp } from 'vue'
+
+import Component from '~target'
+import * as exports from '~target'
+
+export async function main() {
+  const instance = createSSRApp(Component)
+  const ssrContext = {}
+  const markup = await renderToString(instance, ssrContext)
+  return {
+    instance,
+    markup,
+    componentModule: Component,
+    ssrContext,
+  }
+}
diff --git a/test/fixtures/ssr-style.vue b/test/fixtures/ssr-style.vue
new file mode 100644
index 000000000..55d15f674
--- /dev/null
+++ b/test/fixtures/ssr-style.vue
@@ -0,0 +1,25 @@
+<template>
+  <div>
+    <h1>Hello</h1>
+    <basic/>
+    <functional-style/>
+  </div>
+</template>
+
+<script>
+import Basic from './basic.vue'
+import FunctionalStyle from './functional-style.vue'
+
+export default {
+  components: {
+    Basic,
+    FunctionalStyle
+  }
+}
+</script>
+
+<style>
+h1 { color: green; }
+</style>
+
+<style src="./style-import.css"></style>
diff --git a/test/ssr.spec.ts b/test/ssr.spec.ts
new file mode 100644
index 000000000..a21f9afab
--- /dev/null
+++ b/test/ssr.spec.ts
@@ -0,0 +1,79 @@
+import { mockServerBundleAndRun, genId, DEFAULT_VUE_USE } from './utils'
+
+test('SSR style and moduleId extraction', async () => {
+  const { markup, ssrContext } = await mockServerBundleAndRun({
+    entry: 'ssr-style.vue',
+  })
+
+  expect(markup).toContain('<h1>Hello</h1>')
+  expect(markup).toContain('Hello from Component A!')
+  expect(markup).toContain('<div class="foo">functional</div>')
+  // collect component identifiers during render
+  expect(Array.from(ssrContext._registeredComponents).length).toBe(3)
+})
+
+test('SSR with scoped CSS', async () => {
+  const { markup } = await mockServerBundleAndRun({
+    entry: 'scoped-css.vue',
+  })
+
+  const shortId = genId('scoped-css.vue')
+  const id = `data-v-${shortId}`
+
+  expect(markup).toContain(`<div ${id}>`)
+  expect(markup).toContain(`<svg ${id}>`)
+})
+
+test('SSR + CSS Modules', async () => {
+  const testWithIdent = async (
+    localIdentName: string | undefined,
+    regexToMatch: RegExp
+  ) => {
+    const baseLoaders = [
+      'style-loader',
+      {
+        loader: 'css-loader',
+        options: {
+          modules: {
+            localIdentName,
+          },
+        },
+      },
+    ]
+
+    const { componentModule } = await mockServerBundleAndRun({
+      entry: 'css-modules.vue',
+      modify: (config: any) => {
+        config!.module!.rules = [
+          {
+            test: /\.vue$/,
+            use: [DEFAULT_VUE_USE],
+          },
+          {
+            test: /\.css$/,
+            use: baseLoaders,
+          },
+          {
+            test: /\.stylus$/,
+            use: [...baseLoaders, 'stylus-loader'],
+          },
+        ]
+      },
+    })
+
+    const instance = componentModule.__cssModules
+
+    // get local class name
+    const className = instance!.$style.red
+    expect(className).toMatch(regexToMatch)
+  }
+
+  // default ident
+  await testWithIdent(undefined, /^\w{21,}/)
+
+  // custom ident
+  await testWithIdent(
+    '[path][name]---[local]---[hash:base64:5]',
+    /css-modules---red---\w{5}/
+  )
+})
diff --git a/test/utils.ts b/test/utils.ts
index 836f4ef37..046f6a0f3 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,6 +1,7 @@
 /* env jest */
 import * as path from 'path'
 import * as crypto from 'crypto'
+import * as vm from 'vm'
 import webpack from 'webpack'
 import merge from 'webpack-merge'
 // import MiniCssExtractPlugin from 'mini-css-extract-plugin'
@@ -8,6 +9,7 @@ import { fs as mfs } from 'memfs'
 import { JSDOM, VirtualConsole } from 'jsdom'
 import { VueLoaderPlugin } from '..'
 import type { VueLoaderOptions } from '..'
+import type { Component, App } from 'vue'
 
 function hash(text: string): string {
   return crypto.createHash('sha256').update(text).digest('hex').substring(0, 8)
@@ -168,6 +170,34 @@ export function bundle(
   })
 }
 
+export function bundleSSR(
+  options: BundleOptions,
+  wontThrowError?: boolean
+): Promise<{
+  code: string
+  stats: webpack.Stats
+}> {
+  if (typeof options.entry === 'string' && /\.vue/.test(options.entry)) {
+    const vueFile = options.entry
+    options = merge(options, {
+      entry: require.resolve('./fixtures/ssr-entry'),
+      resolve: {
+        alias: {
+          '~target': path.resolve(__dirname, './fixtures', vueFile),
+        },
+      },
+    })
+  }
+  options = merge(options, {
+    target: 'node',
+    output: {
+      libraryTarget: 'commonjs2',
+    },
+    externals: ['vue'],
+  })
+  return bundle(options, wontThrowError)
+}
+
 export async function mockBundleAndRun(
   options: BundleOptions,
   wontThrowError?: boolean
@@ -203,6 +233,52 @@ export async function mockBundleAndRun(
   }
 }
 
+export async function mockServerBundleAndRun(
+  options: BundleOptions,
+  wontThrowError?: boolean
+) {
+  const { code, stats } = await bundleSSR(options, wontThrowError)
+
+  const dom = new JSDOM(
+    `<!DOCTYPE html><html><head></head><body></body></html>`,
+    {
+      runScripts: 'outside-only',
+      virtualConsole: new VirtualConsole(),
+    }
+  )
+
+  const contextObject = {
+    module: {} as NodeModule,
+    require: require,
+    window: dom.window,
+    document: dom.window.document,
+  }
+  vm.runInNewContext(code, contextObject)
+
+  const {
+    instance,
+    markup,
+    componentModule,
+    ssrContext,
+  }: {
+    instance: App<Element>
+    markup: string
+    componentModule: Component & {
+      __cssModules?: { $style: Record<string, string> }
+    }
+    ssrContext: { _registeredComponents: Set<string> }
+  } = await contextObject.module.exports.main()
+
+  return {
+    markup,
+    componentModule,
+    instance,
+    ssrContext,
+    code,
+    stats,
+  }
+}
+
 export function normalizeNewline(input: string): string {
   return input.replace(new RegExp('\r\n', 'g'), '\n')
 }