feat: integrate Jieba support and enhance VC++ redistributable handling for Windows

This commit is contained in:
hy committed 2026-10-21 12:36:45 +08:00
1 parent 0ea1860085
commit e84f8360d5
12 files changed
+151 -108

No files matched your search

+6 -6
View File
@@ -9,9 +9,9 @@ The following DLLs are redistributed unmodified from the official
- `vcruntime140.dll`
- `vcruntime140_1.dll`
They are deployed app-locally next to the `onnxruntime-node` native binaries so
that the ONNX Runtime embedding model can load without requiring a separate
system-wide VC++ Redistributable installation.
They are deployed app-locally next to the `onnxruntime-node` and
`@node-rs/jieba-win32-x64-msvc` native binaries so that ONNX Runtime and Jieba can
load without requiring a separate system-wide VC++ Redistributable installation.
These files are Microsoft's "Visual C++ Redistributable" redistributable code
and are governed by the Microsoft Software License Terms for the Visual Studio
@@ -20,7 +20,7 @@ or repackage these binaries; updates must be sourced from Microsoft.
SHA-256 (for verification):
- msvcp140.dll 0f885b509a685d2bbfa652fed26b5fb31d88fbdab0a978c641d1c7b8aa460aa9
- msvcp140_1.dll bfad5aef4c63a669e3c140655cdfdf395b6c979b400a447bd5dcb65ed8826c3d
- vcruntime140.dll d5e4d9a3e835fa679450145d6a7d94e36573a509317111904d9b3712c30d9066
- msvcp140.dll 0f885b509a685d2bbfa652fed26b5fb31d88fbdab0a978c641d1c7b8aa460aa9
- msvcp140_1.dll bfad5aef4c63a669e3c140655cdfdf395b6c979b400a447bd5dcb65ed8826c3d
- vcruntime140.dll d5e4d9a3e835fa679450145d6a7d94e36573a509317111904d9b3712c30d9066
- vcruntime140_1.dll 1f2d41c4aa5db0bc33ebf7b66d72943a817d7ce6cbe880502a9403823633093f
+1
View File
@@ -17,6 +17,7 @@ prepare 必须在 Apple Silicon Mac;需已信任的 `Ncurator Update Signing`
- electron-builder 以 `release-stage/app` 为唯一应用输入,开发工作区不直接进入安装包。
- 安装包包含构建后的应用、生产依赖与运行资源,排除开发和测试资料、`.env`、私钥、Source Map 及其他平台文件。`app.asar` 与 `VITE_*` 可由用户读取,不作为凭证保护手段。
- 生产依赖按追踪结果裁剪,并验证关键读写路径;追踪触达待裁剪目录时构建失败,避免误删运行依赖。
- Windows x64 在 ONNX Runtime 与 Jieba 原生模块旁各自部署 VC++ 运行库,不要求用户预装系统运行库;安装包审计检查两处实际解包的二进制与 DLL,staging 实际加载 Jieba 默认词典并验证中文分词。
- 网页抓取 sidecar 保留 Playwright 驱动 JS,复用 Electron 提供的 Node 与浏览器,不重复打包这些运行时。构建、缓存复用和打包后检查 sidecar 能否实际启动。
- 候选 bundle 绑定 Tag、commit、fragment、安装包、更新文件与哈希,测试和发布消费同一份可校验产物。
+1
View File
@@ -37,6 +37,7 @@ asarUnpack:
- '**/node_modules/node-pty/prebuilds/**/*'
- '**/node_modules/@napi-rs/canvas/**/*'
- '**/node_modules/@napi-rs/canvas-*/**/*'
- '**/node_modules/@node-rs/jieba-*/**/*'
win:
icon: build/icon.ico
executableName: 馆长
+1 -1
View File
@@ -571,7 +571,7 @@ const main = async () => {
if (audit.status !== 0) throw new Error('staging package security audit failed')
process.stdout.write(
`[package] staging ready for ${process.platform}-${process.arch}; runtime roots: ${runtimePackageNames.join(', ')}; packaged dependencies: ${String(Object.keys(packagedDependencies).length)}; targeted pruning: ${(runtimePruning.totalBytes / 1024 / 1024).toFixed(1)} MiB; runtime smoke: ${String(runtimeVerification.docxDocuments)} DOCX / ${String(runtimeVerification.pdfDocuments)} PDF document(s), MathJax SVG, ExcelJS round trip; obfuscated main: ${String(obfuscation.totals.files)} file(s)\n`
`[package] staging ready for ${process.platform}-${process.arch}; runtime roots: ${runtimePackageNames.join(', ')}; packaged dependencies: ${String(Object.keys(packagedDependencies).length)}; targeted pruning: ${(runtimePruning.totalBytes / 1024 / 1024).toFixed(1)} MiB; runtime smoke: Jieba ${String(runtimeVerification.jiebaTokens)} token(s), ${String(runtimeVerification.docxDocuments)} DOCX / ${String(runtimeVerification.pdfDocuments)} PDF document(s), MathJax SVG, ExcelJS round trip; obfuscated main: ${String(obfuscation.totals.files)} file(s)\n`
)
}
+30 -21
View File
@@ -8,16 +8,25 @@ export const VC_REDIST_DLL_NAMES = [
'vcruntime140_1.dll'
]
const ONNX_BIN_SEGMENTS = ['node_modules', 'onnxruntime-node', 'bin', 'napi-v3', 'win32', 'x64']
const WINDOWS_NATIVE_BINARIES = [
'node_modules/onnxruntime-node/bin/napi-v3/win32/x64/onnxruntime_binding.node',
'node_modules/@node-rs/jieba-win32-x64-msvc/jieba.win32-x64-msvc.node'
]
const isWindowsX64 = (platform, arch) => platform === 'win32' && arch === 'x64'
const assertDirectory = (directory, label) => {
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
throw new Error(`${label} not found: ${directory}`)
const assertFile = (filePath, label) => {
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
throw new Error(`${label} missing: ${filePath}`)
}
}
const resolveNativeBinaries = (root) =>
WINDOWS_NATIVE_BINARIES.map((relativePath) => path.join(root, ...relativePath.split('/')))
export const resolvePackagedWindowsNativeBinaries = (appRoot) =>
resolveNativeBinaries(path.join(appRoot, 'resources', 'app.asar.unpacked'))
export const installWindowsVcRuntime = ({
stageDir,
vcRedistDir,
@@ -26,34 +35,34 @@ export const installWindowsVcRuntime = ({
}) => {
if (!isWindowsX64(platform, arch)) return
assertDirectory(vcRedistDir, 'VC++ redistributable source directory')
const targetDirectory = path.join(stageDir, ...ONNX_BIN_SEGMENTS)
assertDirectory(targetDirectory, 'ONNX Runtime target binary directory')
for (const filename of VC_REDIST_DLL_NAMES) {
const sources = VC_REDIST_DLL_NAMES.map((filename) => {
const source = path.join(vcRedistDir, filename)
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
throw new Error(`VC++ redistributable DLL missing: ${source}`)
assertFile(source, 'VC++ redistributable DLL')
return source
})
const binaries = resolveNativeBinaries(stageDir)
for (const binary of binaries) assertFile(binary, 'Windows native binary')
for (const binary of binaries) {
for (const source of sources) {
fs.copyFileSync(source, path.join(path.dirname(binary), path.basename(source)))
}
fs.copyFileSync(source, path.join(targetDirectory, filename))
}
process.stdout.write('[package] installed VC++ redistributable DLLs into ONNX Runtime binary directory\n')
process.stdout.write(
'[package] installed VC++ redistributable DLLs next to ONNX Runtime and Jieba binaries\n'
)
}
export const resolveVcRuntimeDllDirectory = (appRoot) =>
path.join(appRoot, 'resources', 'app.asar.unpacked', ...ONNX_BIN_SEGMENTS)
export const assertVcRuntimeDllsPackaged = (
appRoot,
{ platform = process.platform, arch = process.arch } = {}
) => {
if (!isWindowsX64(platform, arch)) return
const directory = resolveVcRuntimeDllDirectory(appRoot)
for (const filename of VC_REDIST_DLL_NAMES) {
const dllPath = path.join(directory, filename)
if (!fs.existsSync(dllPath) || !fs.statSync(dllPath).isFile()) {
throw new Error(`packaged VC++ redistributable DLL missing: ${filename}`)
for (const binary of resolvePackagedWindowsNativeBinaries(appRoot)) {
const directory = path.dirname(binary)
for (const filename of VC_REDIST_DLL_NAMES) {
assertFile(path.join(directory, filename), 'packaged VC++ redistributable DLL')
}
assertFile(binary, 'packaged Windows native binary')
}
}
+8
View File
@@ -70,6 +70,13 @@ export const verifyStagedRuntime = async ({ stageDirectory, reportPath, rootDire
}
}
const { Jieba } = loadStageModule(stageRequire, stageNodeModules, '@node-rs/jieba')
const { dict } = loadStageModule(stageRequire, stageNodeModules, '@node-rs/jieba/dict')
const jiebaTokens = Jieba.withDict(dict).cutForSearch('中文分词', true)
if (!jiebaTokens.includes('中文') || !jiebaTokens.includes('分词')) {
throw new Error('Jieba staged native runtime or default dictionary is not working')
}
const docxFixture = path.join(root, 'tests', 'fixtures', 'workspace', 'sample-edit.docx')
const pdfFixture = path.join(root, 'tests', 'fixtures', 'workspace', '学习笔记', '深度学习.pdf')
for (const fixture of [docxFixture, pdfFixture]) {
@@ -187,6 +194,7 @@ export const verifyStagedRuntime = async ({ stageDirectory, reportPath, rootDire
return {
packages: report.packages.length,
requiredFiles: report.requiredFiles.length,
jiebaTokens: jiebaTokens.length,
docxDocuments: docxDocuments.length,
pdfDocuments: pdfDocuments.length,
formulaSvgBytes: Buffer.byteLength(formulaMarkup),
-34
View File
@@ -174,9 +174,6 @@ import type {
import path from 'path'
import fs from 'fs'
import { WEB_INGEST_CONCURRENCY } from '../utils/constant'
import { Jieba } from '@node-rs/jieba'
import { dict } from '@node-rs/jieba/dict'
import { normalizeForIpc } from '../utils/serialization'
import { writeTextFileAtomically } from '../services/files/TextFileWriteService'
import { ConfirmDialogWindowService } from '../services/ConfirmDialogWindowService'
import type { ConfirmDialogRequest, ConfirmDialogResponse } from '../../shared/types'
@@ -222,7 +219,6 @@ const takeE2ENativeDialogSelection = (
return selectedPath
}
const jieba = Jieba.withDict(dict)
const getErrorMessage = (error: unknown): string =>
error instanceof Error ? error.message : String(error)
@@ -2610,36 +2606,6 @@ export function registerHandlers(services: {
}
)
ipcMain.handle(
'search',
async (_event, query: string, sourceType?: 'all' | 'file' | 'web', documentIds?: string[]) => {
try {
const { data: queryVector } = await embeddingService.embed(query)
const filter = sourceType && sourceType !== 'all' ? sourceType : undefined
const results = await documentsStore.search(
queryVector,
query,
5,
filter,
undefined,
documentIds
)
const tokens = jieba.cutForSearch(query, true)
const finalResults = results.map(normalizeForIpc)
return {
results: finalResults,
tokens
}
} catch (error) {
console.error('? [SEARCH] ERROR:', error)
throw error
}
}
)
ipcMain.handle('read-file', async (_event, filePath: string) => {
try {
if (!fs.existsSync(filePath)) {
-7
View File
@@ -93,8 +93,6 @@ import type {
LlmModelListResponse,
PrepareWorkspaceRequest,
RefreshWebSourceRequest,
SearchResponse,
SearchSourceFilter,
PickDirectoryResponse,
PickFileResponse,
ListOpenWithApplicationsRequest,
@@ -190,11 +188,6 @@ export type DesktopApi = {
revealPath: (filePath: string) => Promise<{ success: boolean; error?: string }>
pickDirectory: () => Promise<PickDirectoryResponse>
pickFile: (defaultPath?: string) => Promise<PickFileResponse>
search: (
query: string,
sourceType?: SearchSourceFilter,
documentIds?: string[]
) => Promise<SearchResponse>
workspaceList: () => Promise<WorkspaceListResponse>
workspaceCreate: (payload: CreateWorkspaceSourceRequest) => Promise<WorkspaceMutationResponse>
workspaceSetRootPath: (payload: SetWorkspaceRootPathRequest) => Promise<WorkspaceMutationResponse>
-3
View File
@@ -8,7 +8,6 @@ import type {
AppUpdateSnapshot,
BrowserTabState,
JsonValue,
SearchSourceFilter,
WorkspaceIndexJobRecord
} from '../shared/types'
import {
@@ -204,8 +203,6 @@ const api: DesktopApi = {
})
return ipcRenderer.invoke('open-file-with-application', payload)
},
search: (query: string, sourceType?: SearchSourceFilter, documentIds?: string[]) =>
ipcRenderer.invoke('search', query, sourceType, documentIds),
workspaceList: () => ipcRenderer.invoke('workspace-list'),
workspaceCreate: (payload) => ipcRenderer.invoke('workspace-create', payload),
workspaceSetRootPath: (payload) => ipcRenderer.invoke('workspace-set-root-path', payload),
-6
View File
@@ -2,7 +2,6 @@ import type { AppLanguage, AppRuntimeSnapshot, ServiceRoute } from './appPrefere
export type DocumentSourceType = 'file' | 'web'
export type DocumentImportStatus = 1 | 2 | 3
export type SearchSourceFilter = 'all' | DocumentSourceType
export type WebSourceIdentityKind = 'normalized_url'
export type WebSourceIndexStatus =
| 'needs_index'
@@ -501,11 +500,6 @@ export interface SearchResult {
document?: DocumentRecord
}
export interface SearchResponse {
results: SearchResult[]
tokens: string[]
}
export interface ChunkListItem {
id: string
text: string
@@ -13,7 +13,7 @@ import {
import { PROTECTED_MAIN_PLAINTEXT_MARKERS } from '../../../../scripts/build/package/obfuscation.mjs'
import {
VC_REDIST_DLL_NAMES,
resolveVcRuntimeDllDirectory
resolvePackagedWindowsNativeBinaries
} from '../../../../scripts/build/package/vc-runtime.mjs'
const execFileAsync = promisify(execFile)
@@ -118,10 +118,13 @@ describe('package content policy paths', () => {
await writeFile(nodePtyHelperPath, '', { mode: 0o755 })
await writeFile(sidecarExecutablePath, '', { mode: 0o755 })
await writeFile(playwrightDriverPath, 'module.exports = {}\n')
const vcRuntimeDllDirectory = resolveVcRuntimeDllDirectory(packagedApp)
await mkdir(vcRuntimeDllDirectory, { recursive: true })
for (const filename of VC_REDIST_DLL_NAMES) {
await writeFile(path.join(vcRuntimeDllDirectory, filename), 'dummy')
for (const binary of resolvePackagedWindowsNativeBinaries(packagedApp)) {
const vcRuntimeDllDirectory = path.dirname(binary)
await mkdir(vcRuntimeDllDirectory, { recursive: true })
await writeFile(binary, 'native fixture')
for (const filename of VC_REDIST_DLL_NAMES) {
await writeFile(path.join(vcRuntimeDllDirectory, filename), 'dummy')
}
}
await writeFile(path.join(appSource, 'out', 'main', 'index.js'), 'export const ready = true\n')
await writeFile(
@@ -1,5 +1,5 @@
import { execFile } from 'node:child_process'
import { mkdtemp, mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises'
import { mkdtemp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { promisify } from 'node:util'
@@ -9,7 +9,7 @@ import {
VC_REDIST_DLL_NAMES,
assertVcRuntimeDllsPackaged,
installWindowsVcRuntime,
resolveVcRuntimeDllDirectory
resolvePackagedWindowsNativeBinaries
} from '../../../../scripts/build/package/vc-runtime.mjs'
const execFileAsync = promisify(execFile)
@@ -19,27 +19,66 @@ afterEach(async () => {
if (temporaryDirectory) await rm(temporaryDirectory, { recursive: true, force: true })
})
const writeDummyDll = async (directory, filename) => {
const writeDummyDll = async (directory: string, filename: string) => {
await mkdir(directory, { recursive: true })
await writeFile(path.join(directory, filename), 'dummy')
}
const stageTargetDirectory = (stageDir) =>
path.join(stageDir, 'node_modules', 'onnxruntime-node', 'bin', 'napi-v3', 'win32', 'x64')
const stageNativeBinaries = (stageDir: string) => [
path.join(
stageDir,
'node_modules',
'onnxruntime-node',
'bin',
'napi-v3',
'win32',
'x64',
'onnxruntime_binding.node'
),
path.join(
stageDir,
'node_modules',
'@node-rs',
'jieba-win32-x64-msvc',
'jieba.win32-x64-msvc.node'
)
]
const writeNativeBinaries = async (binaries: string[]) => {
for (const binary of binaries) {
await mkdir(path.dirname(binary), { recursive: true })
await writeFile(binary, 'native fixture')
}
}
const writePackagedRuntime = async (appRoot: string) => {
const binaries = resolvePackagedWindowsNativeBinaries(appRoot)
await writeNativeBinaries(binaries)
for (const binary of binaries) {
for (const filename of VC_REDIST_DLL_NAMES) {
await writeDummyDll(path.dirname(binary), filename)
}
}
return binaries
}
describe('VC++ redistributable packaging', () => {
it('installs the redistributable DLLs next to the ONNX Runtime binaries', async () => {
it('installs the redistributable DLLs next to both ONNX Runtime and Jieba binaries', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const stageDir = path.join(temporaryDirectory, 'stage')
const vcRedistDir = path.join(temporaryDirectory, 'vc-redist')
const targetDir = stageTargetDirectory(stageDir)
await mkdir(targetDir, { recursive: true })
const binaries = stageNativeBinaries(stageDir)
await writeNativeBinaries(binaries)
for (const filename of VC_REDIST_DLL_NAMES) await writeDummyDll(vcRedistDir, filename)
installWindowsVcRuntime({ stageDir, vcRedistDir, platform: 'win32', arch: 'x64' })
for (const filename of VC_REDIST_DLL_NAMES) {
expect((await stat(path.join(targetDir, filename))).isFile()).toBe(true)
for (const binary of binaries) {
for (const filename of VC_REDIST_DLL_NAMES) {
expect(await readFile(path.join(path.dirname(binary), filename))).toEqual(
await readFile(path.join(vcRedistDir, filename))
)
}
}
})
@@ -47,7 +86,7 @@ describe('VC++ redistributable packaging', () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const stageDir = path.join(temporaryDirectory, 'stage')
const vcRedistDir = path.join(temporaryDirectory, 'vc-redist')
await mkdir(stageTargetDirectory(stageDir), { recursive: true })
await writeNativeBinaries(stageNativeBinaries(stageDir))
for (const filename of VC_REDIST_DLL_NAMES.slice(0, -1)) {
await writeDummyDll(vcRedistDir, filename)
}
@@ -57,37 +96,62 @@ describe('VC++ redistributable packaging', () => {
).toThrow('VC++ redistributable DLL missing')
})
it('rejects a stage missing the Windows Jieba optional binary package', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const stageDir = path.join(temporaryDirectory, 'stage')
const vcRedistDir = path.join(temporaryDirectory, 'vc-redist')
await writeNativeBinaries(stageNativeBinaries(stageDir).slice(0, 1))
for (const filename of VC_REDIST_DLL_NAMES) await writeDummyDll(vcRedistDir, filename)
expect(() =>
installWindowsVcRuntime({ stageDir, vcRedistDir, platform: 'win32', arch: 'x64' })
).toThrow('jieba.win32-x64-msvc.node')
})
it('skips installation on non-Windows x64 platforms', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const stageDir = path.join(temporaryDirectory, 'stage')
const vcRedistDir = path.join(temporaryDirectory, 'vc-redist')
const targetDir = stageTargetDirectory(stageDir)
await mkdir(targetDir, { recursive: true })
const binaries = stageNativeBinaries(stageDir)
await writeNativeBinaries(binaries)
installWindowsVcRuntime({ stageDir, vcRedistDir, platform: 'darwin', arch: 'arm64' })
expect(await readdir(targetDir)).toEqual([])
for (const binary of binaries) {
expect(await readdir(path.dirname(binary))).toEqual([path.basename(binary)])
}
})
it('asserts the redistributable DLLs are present in a packaged Windows app', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const appRoot = path.join(temporaryDirectory, 'win-unpacked')
const dllDir = resolveVcRuntimeDllDirectory(appRoot)
for (const filename of VC_REDIST_DLL_NAMES) await writeDummyDll(dllDir, filename)
await writePackagedRuntime(appRoot)
expect(() => assertVcRuntimeDllsPackaged(appRoot, { platform: 'win32', arch: 'x64' })).not.toThrow()
expect(() =>
assertVcRuntimeDllsPackaged(appRoot, { platform: 'win32', arch: 'x64' })
).not.toThrow()
})
it('throws when a packaged Windows app omits a redistributable DLL', async () => {
it.each([0, 1])('rejects a missing DLL in packaged native target %i', async (index) => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const appRoot = path.join(temporaryDirectory, 'win-unpacked')
const dllDir = resolveVcRuntimeDllDirectory(appRoot)
for (const filename of VC_REDIST_DLL_NAMES.slice(0, -1)) {
await writeDummyDll(dllDir, filename)
}
const binaries = await writePackagedRuntime(appRoot)
const omittedDll = path.join(path.dirname(binaries[index]!), 'vcruntime140.dll')
await rm(omittedDll)
expect(() => assertVcRuntimeDllsPackaged(appRoot, { platform: 'win32', arch: 'x64' })).toThrow(
'packaged VC++ redistributable DLL missing'
omittedDll
)
})
it('rejects a packaged Jieba binary that is absent from app.asar.unpacked', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const appRoot = path.join(temporaryDirectory, 'win-unpacked')
const binaries = await writePackagedRuntime(appRoot)
await rm(binaries[1]!)
expect(() => assertVcRuntimeDllsPackaged(appRoot, { platform: 'win32', arch: 'x64' })).toThrow(
'packaged Windows native binary missing'
)
})
@@ -98,7 +162,7 @@ describe('VC++ redistributable packaging', () => {
})
it.skipIf(process.platform !== 'win32' || process.arch !== 'x64')(
'audit-app rejects a Windows package missing the redistributable DLLs',
'audit-app rejects a Windows package with ONNX DLLs but missing Jieba runtime',
async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-vc-runtime-'))
const appSource = path.join(temporaryDirectory, 'app-source')
@@ -106,7 +170,10 @@ describe('VC++ redistributable packaging', () => {
const archivePath = path.join(packageDirectory, 'win-unpacked', 'resources', 'app.asar')
await mkdir(path.join(appSource, 'out', 'main'), { recursive: true })
await mkdir(path.dirname(archivePath), { recursive: true })
await writeFile(path.join(appSource, 'out', 'main', 'index.js'), 'export const ready = true\n')
await writeFile(
path.join(appSource, 'out', 'main', 'index.js'),
'export const ready = true\n'
)
await writeFile(
path.join(appSource, 'package.json'),
`${JSON.stringify({
@@ -118,6 +185,9 @@ describe('VC++ redistributable packaging', () => {
})}\n`
)
await createPackage(appSource, archivePath)
const appRoot = path.join(packageDirectory, 'win-unpacked')
const binaries = await writePackagedRuntime(appRoot)
await rm(path.join(path.dirname(binaries[1]!), 'vcruntime140.dll'))
let stderr = ''
try {
@@ -129,6 +199,7 @@ describe('VC++ redistributable packaging', () => {
stderr = String((error as { stderr?: string }).stderr || error)
}
expect(stderr).toContain('packaged VC++ redistributable DLL missing')
expect(stderr).toContain('jieba-win32-x64-msvc')
}
)
})