Vite SSR & MSW build 失敗

set: 踩坑分享
tags: Vite SSR Svelte
category: Front-End
description: Vite SSR & MSW build 失敗
created_at: 2023/04/11 20:00:00
updated_at: 2023/04/16 14:00:00

cover image


2023.04.16 更

我今天忽然想到,還有 hooks.server 可以用,就不需要那麼麻煩的在 layout 去做(還要判斷是否在 server 環境),後來發現只要在 hooks.server 做就沒事了。

原來是我自己在耍笨,不過至少也學到了解決問題。

所以下面的內容變成了 debug 過程經驗分享,或是說 vite.configresolve.alias 使用案例分享(?)

雖然我還是很想知道為什麼在 layout 做就會出事就是了..


下方內容可能已過時,有興趣再往下閱讀


前言

最近在做課程作業的時候想拿 Svelte(準確來說是 Sveltekit) 當練習,追求完整性的我當然上了 MSW(Mock Service Worker),最後在 Build 的時候居然噴錯了。

而且 github 上也有在討論這個問題,看起來是 Vite 然後 SSR(Server Side Render) 就會碰到這個問題的樣子。

雖然上面的討論沒有得到解答,但至少我今天嘗試的時候讓他 Build 成功了,所以想說寫個一篇記錄一下。

順便廣告一下自己寫的 Svelte 系列,有興趣可以去看看。


重現錯誤

  1. 開啟 Sveltekit 專案(可以參考這篇)
  2. 整合 msw,可以參考官方

懶得看官方慢慢來的話,直接照下面步驟(如果套件沒大改的話應該能動XD)

$ npm i msw -D

建立檔案: src/mocks/handlers.js

import { rest } from 'msw'
export const handlers = [
  rest.get('/user', (req, res, ctx) => {
    return res(
      ctx.status(200)
    )
  }),
]

初始化 service-worker

$ npx msw init static --save

建立檔案: src/mocks/browser.js

import { setupWorker } from 'msw'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)

建立檔案: src/mocks/server.js

import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)

整合 browser,建立: src/hooks.client.js

if (import.meta.env.MODE === 'development') {
  const {worker} = await import('./mocks/browser')
  worker.start()
}

這時你可以先 Build 看看,理論上會成功,因為問題出在 SSR,跟 CSR 無關

$ npm run build

再來整合 server,建立: src/routes/+layout.js

import { browser } from '$app/environment'

if(!browser && process.env.NODE_ENV === 'development') {
  const { server } = await import('../mocks/server')
  server.listen()
}

這時候 Build 就會出事了

"setTimeout" is not exported by "__vite-browser-external", imported by "node_modules/msw/lib/node/index.mjs".
file: /node_modules/msw/lib/node/index.mjs:28:9
26: 
27: // config/polyfills-node.ts
28: import { setTimeout as nodeSetTimeout } from "timers";
             ^
29: var setTimeout = nodeSetTimeout;
[vite-plugin-sveltekit-compile] "setTimeout" is not exported by "__vite-browser-external", imported by "node_modules/msw/lib/node/index.mjs".
file: /node_modules/msw/lib/node/index.mjs:28:9
26: 
27: // config/polyfills-node.ts
28: import { setTimeout as nodeSetTimeout } from "timers";
             ^
29: var setTimeout = nodeSetTimeout;
✓ built in 2.87s
error during build:
RollupError: "setTimeout" is not exported by "__vite-browser-external", imported by "node_modules/msw/lib/node/index.mjs".
    at error (file:////node_modules/rollup/dist/es/shared/node-entry.js:2128:30)
    at Module.error (file:////node_modules/rollup/dist/es/shared/node-entry.js:13322:16)
    at Module.traceVariable (file:////node_modules/rollup/dist/es/shared/node-entry.js:13707:29)
    at ModuleScope.findVariable (file:////node_modules/rollup/dist/es/shared/node-entry.js:12210:39)
    at Identifier.bind (file:////node_modules/rollup/dist/es/shared/node-entry.js:8103:40)
    at VariableDeclarator.bind (file:////node_modules/rollup/dist/es/shared/node-entry.js:5749:23)
    at VariableDeclaration.bind (file:////node_modules/rollup/dist/es/shared/node-entry.js:5745:28)
    at Program.bind (file:////node_modules/rollup/dist/es/shared/node-entry.js:5745:28)
    at Module.bindReferences (file:////node_modules/rollup/dist/es/shared/node-entry.js:13318:18)
    at Graph.sortModules (file:////node_modules/rollup/dist/es/shared/node-entry.js:24704:20)

解決過程

看看錯誤訊息,關鍵就是 setTimeout 找不到,然後進去看原始碼:

// config/polyfills-node.ts
import { setTimeout as nodeSetTimeout } from "timers";
var setTimeout = nodeSetTimeout;

他是死在 import { setTimeout as nodeSetTimeout } from "timers"; 這一行,不太清楚為什麼他的 polyfills 沒有正常運作,我也去嘗試過一些 polyfill 相關的套件,但也是沒有用。

於是我就稍微測一下上面那段 code 真的有問題嗎?

建立檔案: test.js

import { setTimeout as nodeSetTimeout } from "timers";
var setTimeout = nodeSetTimeout;
console.log(setTimeout)

然後執行他:

$ node test.js

輸出:

[Function: setTimeout] {
  [Symbol(nodejs.util.promisify.custom)]: [Getter]
}

於是我就想到那我把 vite 加入一個 aliasresolve 的時候是不是就可以解決?

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [sveltekit()],
    resolve: {
        alias: {
            'timers': '/test'
        }
    }
});

: 因為目前還在測試,所以路徑弄得很醜,可以自己調整把他弄漂亮一些

這時再改一下 test.js,因為剛才只有輸出並沒有 export

import { setTimeout as nodeSetTimeout } from "timers";
var setTimeout = nodeSetTimeout;
export {
    setTimeout
}

這時就成功了,超感動Der,於是再改成下面的寫法,因為總不能這樣寫死,這樣不好。

export * from "timers";

這時居然就失敗了?!!!,我先懷疑是我自己語法下錯,所以我又開了一個檔案 time.js

import { setTimeout } from './test.js'
console.log(setTimeout)

這時執行 node time.js,會輸出:

[Function: setTimeout] {
  [Symbol(nodejs.util.promisify.custom)]: [Getter]
}

WTF?!,沒道理阿,我是不是哪裡搞錯了什麼?

這時我又嘗試下面這樣:

import { setTimeout } from "timers";

export {
    setTimeout
}

這樣居然也是失敗??

不就差一個 var setTimeout = nodeSetTimeout; 而已嗎..

於是我嘗試了很久,最後採用這個方式解決(雖然還是有點寫死..,幸好這個模組提供的方法不多):

const { 
    setTimeout,
    clearTimeout,
    setInterval,
    clearInterval,
    setImmediate,
    clearImmediate 
} = await import('timers');

export { 
    setTimeout, 
    clearTimeout, 
    setInterval, 
    clearInterval, 
    setImmediate, 
    clearImmediate 
};

順帶一提,下面這樣居然不行哦XD

import { 
    setTimeout,
    clearTimeout,
    setInterval,
    clearInterval,
    setImmediate,
    clearImmediate 
} from 'timers';

export { 
    setTimeout, 
    clearTimeout, 
    setInterval, 
    clearInterval, 
    setImmediate, 
    clearImmediate 
};

靜態 import 不行, 動態 import 才可以,唉..嘆氣。

總之就到這邊解決了。終於 Build 得起來了。


解決懶人包(乾淨版)

  1. 建立檔案: src/polyfills/timers.js
const { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate } =
    await import('timers');

export { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate };

: 也可以使用 .ts

  1. 修改 vite.config.[js|ts]
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  // 略
  resolve: {
    // 略
    alias: {
      // 略
      timers: fileURLToPath(new URL('./src', import.meta.url) + '/polyfills/timers.js')
    }
  }
})

總結

解決辦法很簡單,不過沒辦法立刻找到現成解法就需要花一點時間去研究,希望可以幫到碰到一樣問題的人,或是之後官方直接解決,就不用那麼麻煩了。




最後更新時間: 2023年04月16日.