Worklog for task "Add code analysis features"
Managed to dig out some old developments, here is a minimal set of useful methods.
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
}
]
}
}
}
Now the problem is that our TypeScript server starts on every request.
import path from 'path'
import ts from 'typescript'
export interface ProgramContext {
program: ts.Program
checker: ts.TypeChecker
/** Dir base directory (the folder that contains tsconfig.json). */
baseDir: string
}
/**
* Build a TypeScript Program from the project's tsconfig.json.
* Rebuilt per call — TS's incremental cache is internal to a single Program,
* and the host process may pick up file changes between requests.
*/
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,
)
/*
Here we create the TS server and it's quite a heavy operation,
it not only consumes a lot of CPU and memory, but also introduces noticeable delays
*/
const program = ts.createProgram({
rootNames: parsed.fileNames,
options: parsed.options,
})
return {
program,
checker: program.getTypeChecker(),
baseDir,
}
}
We need to be able to keep longer-lived TS server instances so we don't have to start from scratch every time. There are a few options here (according to opus 4.5):
1. ts.createIncrementalProgram / ts.createWatchProgram
TypeScript's built-in mechanism for incremental compilation:
const host = ts.createWatchCompilerHost(configPath, {}, ts.sys);
const watchProgram = ts.createWatchProgram(host);
const program = watchProgram.getProgram().getProgram();
Caches the AST and recompiles only modified files.
2. ts.server.ProjectService (Language Service)
This is what VS Code uses. A full-fledged language server with caching:
import * as ts from 'typescript/lib/tsserverlibrary'
const projectService = new ts.server.ProjectService({ ... })
Heavier, but gives access to an already running tsserver.
3. Connecting to an existing tsserver via LSP
If tsserver is already running (e.g., in an IDE), you can communicate with it via stdio or node-ipc. But this requires knowing where it is listening.
4. Simple application-level cache
Store program in memory and recreate it only when files change:
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,
};
}
}
Recommendation: If you need quick results — Option 4 (simple cache). If you need full IDE integration — Option 2 or 3, but this is significantly more complex.