Nhật ký công việc cho nhiệm vụ "Thêm tính năng phân tích mã nguồn"
Đã lấy được các phần việc cũ, đây là tập hợp tối thiểu các phương thức hữu ích.
query analyzeDir {
analyzeDir(rootDir: "src/components/pages/_App/")
}
{
"data": {
"analyzeDir": {
"rootDir": "src/components/pages/_App",
"count": 3,
"files": [
{
"path": "src/components/pages/_App/getInitialProps.ts"
},
{
"path": "src/components/pages/_App/index.tsx"
},
{
"path": "src/components/pages/_App/interfaces.ts"
}
]
}
}
}
query analyzeFile {
analyzeFile(path: "src/components/pages/_App/getInitialProps.ts") {
path
imports {
default
isExternalLibraryImport
module
namespace
resolvedPath
named {
name
alias
}
}
exports
}
}
{
"data": {
"analyzeFile": {
"path": "src/components/pages/_App/getInitialProps.ts",
"imports": [
{
"default": null,
"isExternalLibraryImport": false,
"module": "src/gql/apolloClient",
"namespace": null,
"resolvedPath": "src/gql/apolloClient/index.ts",
"named": [
{
"name": "initializeApollo",
"alias": null
}
]
},
{
"default": "NextApp",
"isExternalLibraryImport": true,
"module": "next/app",
"namespace": null,
"resolvedPath": null,
"named": []
},
{
"default": null,
"isExternalLibraryImport": false,
"module": "./interfaces",
"namespace": null,
"resolvedPath": "src/components/pages/_App/interfaces.ts",
"named": [
{
"name": "AppInitialProps",
"alias": null
},
{
"name": "MainApp",
"alias": null
},
{
"name": "NextPageContextCustom",
"alias": null
},
{
"name": "PageProps",
"alias": null
},
{
"name": "withWs",
"alias": null
}
]
},
{
"default": null,
"isExternalLibraryImport": false,
"module": "src/helpers/getSiteOrigin",
"namespace": null,
"resolvedPath": "src/helpers/getSiteOrigin.ts",
"named": [
{
"name": "getSiteOrigin",
"alias": null
}
]
}
],
"exports": [
{
"name": "getInitialProps",
"kind": "Function",
"line": 13
}
]
}
}
}
Nhưng vấn đề hiện tại là TypeScript server của chúng ta được khởi chạy trên mỗi request.
import path from 'path'
import ts from 'typescript'
export interface ProgramContext {
program: ts.Program
checker: ts.TypeChecker
/** Dir base directory (thư mục chứa tsconfig.json). */
baseDir: string
}
/**
* Xây dựng một Chương trình TypeScript từ tsconfig.json của dự án.
* Được xây dựng lại cho mỗi lần gọi — bộ đệm tăng dần của TS nằm bên trong một Chương trình duy nhất,
* và tiến trình máy chủ có thể nhận biết các thay đổi tệp giữa các request.
*/
export function getProgram(): ProgramContext {
const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json')
if (!configPath) {
throw new Error('tsconfig.json not found')
}
const baseDir = path.resolve(path.dirname(configPath))
const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
const parsed = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
baseDir,
)
/*
Ngay tại đây chúng ta tạo ts-server và đây là một thao tác khá nặng nề,
nó không chỉ ngốn nhiều CPU và bộ nhớ mà còn gây ra độ trễ đáng kể
*/
const program = ts.createProgram({
rootNames: parsed.fileNames,
options: parsed.options,
})
return {
program,
checker: program.getTypeChecker(),
baseDir,
}
}
Chúng ta cần có khả năng duy trì các instance ts-server lâu hơn để không phải khởi động lại từ đầu mỗi lần. Và đây là một vài lựa chọn (theo opus 4.5):
1. ts.createIncrementalProgram / ts.createWatchProgram
Cơ chế tích hợp sẵn của TypeScript để biên dịch tăng dần:
const host = ts.createWatchCompilerHost(configPath, {}, ts.sys);
const watchProgram = ts.createWatchProgram(host);
const program = watchProgram.getProgram().getProgram();
Cache AST và chỉ biên dịch lại các tệp đã thay đổi.
2. ts.server.ProjectService (Language Service)
Đây là thứ mà VS Code sử dụng. Một language server đầy đủ tính năng có hỗ trợ cache:
import * as ts from 'typescript/lib/tsserverlibrary'
const projectService = new ts.server.ProjectService({ ... })
Nặng hơn, nhưng cung cấp quyền truy cập vào tsserver đang chạy.
3. Kết nối với tsserver hiện có qua LSP
Nếu tsserver đang chạy (ví dụ: trong IDE), bạn có thể giao tiếp với nó qua stdio hoặc node-ipc. Nhưng điều này đòi hỏi phải biết nó đang lắng nghe ở đâu.
4. Cache đơn giản ở cấp độ ứng dụng
Lưu trữ program trong bộ nhớ và chỉ tạo lại khi các tệp thay đổi:
let cachedProgram: ts.Program | null = null;
let lastModified: number = 0;
export function getProgram(baseDir: string) {
const currentModified = getMaxMtime(baseDir);
if (cachedProgram && currentModified <= lastModified) {
return {
program: cachedProgram,
checker: cachedProgram.getTypeChecker(),
baseDir,
};
}
}
Khuyến nghị: Nếu bạn cần kết quả nhanh — phương án 4 (cache đơn giản). Nếu bạn cần tích hợp đầy đủ với IDE — phương án 2 hoặc 3, nhưng điều này phức tạp hơn đáng kể.