kimi-code/apps/kimi-code/scripts/native/assets.mjs
liruifengv 0ec5bd62c0 chore(kimi-code): upgrade pi-tui to 0.78.1 and adapt native helpers
Bump @earendil-works/pi-tui from ^0.74.0 to ^0.78.1. pi-tui 0.75.5 replaced its koffi-based Windows VT input with a bundled native helper, and 0.76.0 added a darwin native helper for Terminal.app Shift+Enter.

- SEA build: teach native-deps to collect pi-tui's per-target .node files, drop the koffi registry, and add a native-file-only collect mode so only package.json + the target .node ship (28 -> 2 files).

- Redirect pi-tui's absolute-path native require() into the native-asset cache through the Module._load hook, and extend the native smoke test to actually load the helper.

- npm package: ship pi-tui's native/ directory so macOS Terminal.app Shift+Enter and Windows Shift+Tab keep working for npm installs.
2026-06-29 19:38:00 +08:00

278 lines
8.1 KiB
JavaScript

import { createHash } from 'node:crypto';
import { existsSync, realpathSync } from 'node:fs';
import { readdir, readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { NATIVE_ASSET_MANIFEST_VERSION, buildManifestKey } from './manifest.mjs';
import { resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';
export { NATIVE_ASSET_MANIFEST_VERSION };
// Re-export for any external consumer that still needs it. Internally we
// use resolveTargetDeps() exclusively — no more if/else against package names.
export const NATIVE_TARGETS = Object.freeze(
Object.fromEntries(
SUPPORTED_TARGETS.map((t) => {
const deps = resolveTargetDeps(t);
const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName;
return [t, { clipboardPackage: clipboardTarget }];
}),
),
);
const jsExtensions = ['.js', '.cjs', '.mjs', '.json', '.node'];
const runtimeEntryNames = ['index.js', 'index.cjs', 'index.mjs'];
function fail(message) {
throw new Error(message);
}
function toPosixPath(path) {
return path.split('\\').join('/');
}
function sha256(bytes) {
return createHash('sha256').update(bytes).digest('hex');
}
async function readJson(path) {
return JSON.parse(await readFile(path, 'utf-8'));
}
async function listFiles(root) {
const files = [];
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
await walk(path);
continue;
}
if (entry.isFile()) {
files.push(path);
}
}
}
await walk(root);
return files;
}
function resolvePackageRootGeneric(requireFromApp, packageName, parentPackageName, appRoot, target) {
try {
return dirname(requireFromApp.resolve(`${packageName}/package.json`));
} catch (rootError) {
if (parentPackageName !== null) {
try {
const parentPackageJsonPath = realpathSync(
requireFromApp.resolve(`${parentPackageName}/package.json`),
);
const requireFromParent = createRequire(pathToFileURL(parentPackageJsonPath));
return dirname(requireFromParent.resolve(`${packageName}/package.json`));
} catch {}
}
fail(
[
`Native asset package is not installed for target ${target}: ${packageName}`,
parentPackageName ? `Searched via parent: ${parentPackageName}` : '',
`Resolve root: ${appRoot}`,
'Run pnpm install --frozen-lockfile before building native assets.',
rootError instanceof Error ? rootError.message : String(rootError),
]
.filter(Boolean)
.join('\n'),
);
}
}
function resolveFileCandidate(path) {
if (existsSync(path)) return path;
for (const extension of jsExtensions) {
const candidate = `${path}${extension}`;
if (existsSync(candidate)) return candidate;
}
for (const entryName of runtimeEntryNames) {
const candidate = join(path, entryName);
if (existsSync(candidate)) return candidate;
}
return null;
}
function resolvePackageEntry(packageRoot, packageJson) {
const rawMain =
typeof packageJson.main === 'string'
? packageJson.main
: typeof packageJson.module === 'string'
? packageJson.module
: 'index.js';
return resolveFileCandidate(resolve(packageRoot, rawMain));
}
function relativeRuntimeSpecifiers(text) {
const specifiers = new Set();
for (const match of text.matchAll(/\brequire\(\s*["'](\.[^"']+)["']\s*\)/g)) {
specifiers.add(match[1]);
}
for (const match of text.matchAll(/(?<![.\w])import\(\s*["'](\.[^"']+)["']\s*\)/g)) {
specifiers.add(match[1]);
}
for (const match of text.matchAll(/\bfrom\s+["'](\.[^"']+)["']/g)) {
specifiers.add(match[1]);
}
return [...specifiers];
}
async function addRuntimeDependencyFiles(packageRoot, filePath, selected) {
const extension = extname(filePath);
if (!['.js', '.cjs', '.mjs'].includes(extension)) return;
let text;
try {
text = await readFile(filePath, 'utf-8');
} catch {
return;
}
for (const specifier of relativeRuntimeSpecifiers(text)) {
const candidate = resolveFileCandidate(resolve(dirname(filePath), specifier));
if (candidate === null) continue;
if (candidate.endsWith('.node')) continue;
const packageRelativePath = relative(packageRoot, candidate);
if (
packageRelativePath.startsWith('..') ||
isAbsolute(packageRelativePath) ||
packageRelativePath.length === 0
) {
continue;
}
if (selected.has(candidate)) continue;
selected.add(candidate);
await addRuntimeDependencyFiles(packageRoot, candidate, selected);
}
}
async function collectPackageFiles({
packageName,
packageRoot,
includeNativeFiles,
includeEntryJs = true,
nativeFileRelatives = [],
}) {
const packageJsonPath = join(packageRoot, 'package.json');
const packageJson = await readJson(packageJsonPath);
const selected = new Set([packageJsonPath]);
if (includeEntryJs) {
const entry = resolvePackageEntry(packageRoot, packageJson);
if (entry !== null) {
selected.add(entry);
await addRuntimeDependencyFiles(packageRoot, entry, selected);
}
}
for (const nativeFileRelative of nativeFileRelatives) {
const nativeFile = resolve(packageRoot, nativeFileRelative);
if (!existsSync(nativeFile)) {
fail(`Native package ${packageName} does not contain ${nativeFileRelative} at ${packageRoot}`);
}
selected.add(nativeFile);
}
if (includeNativeFiles) {
const files = await listFiles(packageRoot);
for (const file of files) {
if (file.endsWith('.node')) {
selected.add(file);
}
}
}
const sorted = [...selected].sort((a, b) => a.localeCompare(b));
if (includeNativeFiles && !sorted.some((file) => file.endsWith('.node'))) {
fail(`Native package ${packageName} does not contain a .node file at ${packageRoot}`);
}
return sorted;
}
async function packageManifestEntries({ packageName, packageRoot, files, target }) {
const root = `node_modules/${packageName}`;
const entries = [];
const assets = {};
for (const file of files) {
const sourceBytes = await readFile(file);
const packageRelativePath = toPosixPath(relative(packageRoot, file));
const relativePath = `${root}/${packageRelativePath}`;
const assetKey = `native/${target}/${relativePath}`;
entries.push({
assetKey,
relativePath,
sha256: sha256(sourceBytes),
});
assets[assetKey] = file;
}
return {
packageManifest: {
name: packageName,
root,
files: entries,
},
assets,
};
}
export const nativeAssetManifestKey = buildManifestKey;
export function nativeAssetSummary(manifest) {
return manifest.packages.map((pkg) => `${pkg.name}: ${pkg.files.length} files`);
}
export async function collectNativeAssets({ appRoot, target }) {
const requireFromApp = createRequire(pathToFileURL(resolve(appRoot, 'package.json')));
const targetDeps = resolveTargetDeps(target); // throws on unsupported target
const manifestPackages = [];
const assets = {};
for (const dep of targetDeps) {
const packageRoot = resolvePackageRootGeneric(
requireFromApp,
dep.resolvedName,
dep.parentName,
appRoot,
target,
);
const files = await collectPackageFiles({
packageName: dep.resolvedName,
packageRoot,
includeNativeFiles: dep.collect === 'native-files',
includeEntryJs: dep.collect !== 'native-file-only',
nativeFileRelatives: dep.nativeFileRelatives,
});
const result = await packageManifestEntries({
packageName: dep.resolvedName,
packageRoot,
files,
target,
});
manifestPackages.push(result.packageManifest);
Object.assign(assets, result.assets);
}
const manifest = {
version: NATIVE_ASSET_MANIFEST_VERSION,
target,
packages: manifestPackages,
};
return {
manifest,
manifestJson: `${JSON.stringify(manifest, null, 2)}\n`,
assets,
};
}