Files
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

224 lines
8.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import protobufVitePlugin from '@snowluma/proton/vite';
import fs from 'fs';
import { builtinModules } from 'module';
import path from 'path';
import { fileURLToPath } from 'url';
import { defineConfig, PluginOption, UserConfig } from 'vite';
import cp from 'vite-plugin-cp';
import { sqliteBuildPlugin } from '@snowluma/sqlite/vite';
import { APP_VERSION } from '@snowluma/common/version';
import runtimePackage from '../runtime/package.json' with { type: 'json' };
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..', '..');
const distDir = path.resolve(repoRoot, 'dist');
const runtimeDir = path.resolve(repoRoot, 'packages', 'runtime');
const nativeDir = path.resolve(runtimeDir, 'native');
// vite-plugin-cp consumes globs through globby; on Windows we must use POSIX-style separators.
const toPosix = (p: string) => p.replace(/\\/g, '/');
// `@snowluma/websocket` is bundled — it's an
// in-tree TS workspace wrapper that routes its native `.node`
// addon through `dist/native/`. Only Node builtins stay external.
//
const nodeModules = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)].flat();
const runtimeSrc = toPosix(runtimeDir);
const nativeSrc = toPosix(nativeDir);
// Target selection: `SNOWLUMA_TARGET=<platform>-<arch>` overrides the host
// detection, enabling cross-target packaging on CI.
const targetTriple = process.env.SNOWLUMA_TARGET ?? `${process.platform}-${process.arch}`;
const [targetPlatform, targetArch] = targetTriple.split('-');
// Runtime scaffolding files copied into dist/. Windows and Linux use different
// launchers, while both targets ship their matching QQ integration binaries.
const runtimeDistFiles = [
'check-node-version.cjs',
targetPlatform === 'win32' ? 'launcher.bat' : 'launcher.sh',
];
const agreementFiles = ['EULA.md', 'PRIVACY.md'];
// Native binaries shipped for the selected target:
// * `snowluma-*.{dll,node,so}` – NTQQ hook (Windows + Linux).
// * `websocket-*.node` – RFC 6455 framing/mask addon (all platforms).
const nativeFiles = [
`websocket-${targetTriple}.node`,
...(targetPlatform === 'win32'
? [`snowluma-${targetTriple}.dll`, `snowluma-${targetTriple}.node`]
: []),
...(targetPlatform === 'linux'
? [`snowluma-${targetTriple}.node`, `snowluma-${targetTriple}.so`]
: []),
];
// FFmpeg native addon ported from NapCat. Uses NapCat's
// `<platform>.<arch>` naming so the prebuilt binaries can be vendored
// as-is without renaming. Lives under `native/ffmpeg/` to keep it
// separate from the SnowLuma flat-naming addons above.
const ffmpegAddonFile = `ffmpegAddon.${targetPlatform}.${targetArch}.node`;
const ffmpegSrcDir = path.resolve(nativeDir, 'ffmpeg');
// Fail fast if any expected native binary is missing from packages/runtime/native/.
// vite-plugin-cp only emits a warning on missing source files; we want a hard
// error so CI can't accidentally ship an incomplete archive.
const missingNatives = nativeFiles.filter((f) => !fs.existsSync(path.join(nativeDir, f)));
if (!fs.existsSync(path.join(ffmpegSrcDir, ffmpegAddonFile))) {
missingNatives.push(`ffmpeg/${ffmpegAddonFile}`);
}
if (missingNatives.length > 0) {
throw new Error(
`Missing native binaries for target ${targetTriple}:\n` +
missingNatives.map((f) => ` - ${path.join(nativeDir, f)}`).join('\n'),
);
}
// Legal docs are served by the WebUI consent gate; a missing one would silently
// disable consent in the shipped build, so fail the build instead (mirrors the
// native-binary guard above).
const missingAgreements = agreementFiles.filter((f) => !fs.existsSync(path.resolve(repoRoot, f)));
if (missingAgreements.length > 0) {
throw new Error(
`Missing legal documents required by the WebUI consent gate:\n` +
missingAgreements.map((f) => ` - ${path.resolve(repoRoot, f)}`).join('\n'),
);
}
const BaseConfigPlugin: PluginOption[] = [
{
name: 'release-metadata',
generateBundle(_options, bundle) {
const entry = bundle['index.mjs'];
if (entry?.type !== 'chunk' || entry.imports.some((id) => !nodeModules.includes(id))) {
this.error('The update entry must only import Node builtins.');
}
this.emitFile({
type: 'asset',
fileName: 'package.json',
source: JSON.stringify({ ...runtimePackage, version: APP_VERSION }, null, 2) + '\n',
});
if (process.env.BUILD_WEBUI === 'true') {
const entries = [
...new Set([
...Object.keys(bundle).map((file) => file.split('/')[0]),
...runtimeDistFiles,
...agreementFiles,
'native',
'client',
'package.json',
'release.json',
]),
].sort();
this.emitFile({
type: 'asset',
fileName: 'release.json',
source:
JSON.stringify(
{ version: APP_VERSION, target: targetTriple, entries },
null,
2,
) + '\n',
});
}
},
},
// Proton substitutes every `protobuf_encode<T>` / `protobuf_decode<T>`
// call site with a monomorphized codec at build time. WITHOUT this
// plugin in the plugins array the runtime fallback in
// `@snowluma/proton/runtime.ts` throws on the first call (and the
// packaged Win/Linux release does exactly that). It MUST appear
// before `cp` so the transform runs on every source file Vite asks
// for, regardless of where `cp` later copies emitted assets.
protobufVitePlugin(),
sqliteBuildPlugin(targetTriple),
cp({
targets: [
...runtimeDistFiles.map((file) => ({
src: `${runtimeSrc}/${file}`,
dest: distDir,
flatten: true,
})),
...nativeFiles.map((f) => ({
src: `${nativeSrc}/${f}`,
dest: path.join(distDir, 'native'),
flatten: true,
})),
{
src: `${toPosix(ffmpegSrcDir)}/${ffmpegAddonFile}`,
dest: path.join(distDir, 'native', 'ffmpeg'),
flatten: true,
},
// Legal docs served verbatim by the WebUI consent gate. Copied next to
// the bundle so consent.ts's path probe finds them in a packaged deploy.
...agreementFiles.map((file) => ({
src: toPosix(path.resolve(repoRoot, file)),
dest: distDir,
flatten: true,
})),
],
}),
];
const BaseConfig = (source_map: boolean = false) =>
defineConfig({
resolve: {
conditions: ['node', 'default'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
sourcemap: source_map,
target: 'node24',
minify: false,
lib: {
entry: {
index: path.resolve(__dirname, 'src/index.ts'),
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
external: nodeModules,
output: {
// Keep recovery runnable while an interrupted update is being restored.
codeSplitting: false,
// Bundled CommonJS dependencies still need Node's native require.
banner: [
"import { createRequire as __snowlumaCreateRequire } from 'node:module';",
'const require = __snowlumaCreateRequire(import.meta.url);',
].join('\n'),
},
},
// Emit to monorepo root dist/ so the existing release pipeline keeps working.
outDir: distDir,
// Required since outDir is outside the vite project root.
emptyOutDir: true,
},
define: {
__BUILD_WEBUI__: process.env.BUILD_WEBUI === 'true',
__PACKAGED__: true,
},
});
export default defineConfig(({ mode }): UserConfig => {
if (mode === 'development') {
return {
...BaseConfig(true),
plugins: [...BaseConfigPlugin],
};
}
return {
...BaseConfig(),
plugins: [...BaseConfigPlugin],
};
});