Improve check error performance by off-loading to worker thread w/ incremental compilation (#575)
This commit is contained in:
@@ -22,7 +22,8 @@ const ignore = (file: string) => {
|
||||
if (file.startsWith("/scaffold")) {
|
||||
return false;
|
||||
}
|
||||
if (file.startsWith("/worker")) {
|
||||
|
||||
if (file.startsWith("/worker") && !file.startsWith("/workers")) {
|
||||
return false;
|
||||
}
|
||||
if (file.startsWith("/node_modules/stacktrace-js")) {
|
||||
@@ -121,6 +122,11 @@ const config: ForgeConfig = {
|
||||
config: "vite.preload.config.mts",
|
||||
target: "preload",
|
||||
},
|
||||
{
|
||||
entry: "workers/tsc/tsc_worker.ts",
|
||||
config: "vite.worker.config.mts",
|
||||
target: "main",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"package": "npm run clean && electron-forge package",
|
||||
"make": "npm run clean && electron-forge make",
|
||||
"publish": "npm run clean && electron-forge publish",
|
||||
"ts": "npx tsc -p tsconfig.app.json --noEmit",
|
||||
"ts": "npm run ts:main && npm run ts:workers",
|
||||
"ts:main": "npx tsc -p tsconfig.app.json --noEmit",
|
||||
"ts:workers": "npx tsc -p workers/tsc/tsconfig.json --noEmit",
|
||||
"lint": "npx oxlint --fix",
|
||||
"lint:fix": "npx oxlint --fix --fix-suggestions --fix-dangerously",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import {
|
||||
getDyadWriteTags,
|
||||
getDyadRenameTags,
|
||||
getDyadDeleteTags,
|
||||
} from "../ipc/processors/response_processor";
|
||||
import { normalizePath } from "../ipc/processors/normalizePath";
|
||||
|
||||
import log from "electron-log";
|
||||
|
||||
const logger = log.scope("VirtualFileSystem");
|
||||
|
||||
export interface VirtualFile {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface VirtualRename {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface SyncFileSystemDelegate {
|
||||
fileExists?: (fileName: string) => boolean;
|
||||
readFile?: (fileName: string) => string | undefined;
|
||||
}
|
||||
SyncFileSystemDelegate,
|
||||
SyncVirtualFileSystem,
|
||||
VirtualChanges,
|
||||
VirtualFile,
|
||||
} from "./tsc_types";
|
||||
import { normalizePath } from "./normalizePath";
|
||||
|
||||
export interface AsyncFileSystemDelegate {
|
||||
fileExists?: (fileName: string) => Promise<boolean>;
|
||||
@@ -78,11 +61,11 @@ export abstract class BaseVirtualFileSystem {
|
||||
/**
|
||||
* Apply changes from a response containing dyad tags
|
||||
*/
|
||||
public applyResponseChanges(fullResponse: string): void {
|
||||
const writeTags = getDyadWriteTags(fullResponse);
|
||||
const renameTags = getDyadRenameTags(fullResponse);
|
||||
const deletePaths = getDyadDeleteTags(fullResponse);
|
||||
|
||||
public applyResponseChanges({
|
||||
deletePaths,
|
||||
renameTags,
|
||||
writeTags,
|
||||
}: VirtualChanges): void {
|
||||
// Process deletions
|
||||
for (const deletePath of deletePaths) {
|
||||
this.deleteFile(deletePath);
|
||||
@@ -147,7 +130,7 @@ export abstract class BaseVirtualFileSystem {
|
||||
this.virtualFiles.set(toNormalized, content);
|
||||
} catch (error) {
|
||||
// If we can't read the source file, we'll let the consumer handle it
|
||||
logger.warn(
|
||||
console.warn(
|
||||
`Could not read source file for rename: ${fromPath}`,
|
||||
error,
|
||||
);
|
||||
@@ -214,7 +197,10 @@ export abstract class BaseVirtualFileSystem {
|
||||
/**
|
||||
* Synchronous virtual filesystem
|
||||
*/
|
||||
export class SyncVirtualFileSystem extends BaseVirtualFileSystem {
|
||||
export class SyncVirtualFileSystemImpl
|
||||
extends BaseVirtualFileSystem
|
||||
implements SyncVirtualFileSystem
|
||||
{
|
||||
private delegate: SyncFileSystemDelegate;
|
||||
|
||||
constructor(baseDir: string, delegate?: SyncFileSystemDelegate) {
|
||||
52
shared/tsc_types.ts
Normal file
52
shared/tsc_types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export interface SyncVirtualFileSystem {
|
||||
fileExists: (fileName: string) => boolean;
|
||||
readFile: (fileName: string) => string | undefined;
|
||||
|
||||
getVirtualFiles: () => { path: string }[];
|
||||
getDeletedFiles: () => string[];
|
||||
}
|
||||
|
||||
export interface SyncFileSystemDelegate {
|
||||
fileExists?: (fileName: string) => boolean;
|
||||
readFile?: (fileName: string) => string | undefined;
|
||||
}
|
||||
|
||||
export interface Problem {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface ProblemReport {
|
||||
problems: Problem[];
|
||||
}
|
||||
|
||||
export interface WorkerInput {
|
||||
appPath: string;
|
||||
virtualChanges: VirtualChanges;
|
||||
tsBuildInfoCacheDir: string;
|
||||
}
|
||||
|
||||
export interface WorkerOutput {
|
||||
success: boolean;
|
||||
data?: ProblemReport;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface VirtualChanges {
|
||||
deletePaths: string[];
|
||||
renameTags: VirtualRename[];
|
||||
writeTags: VirtualFile[];
|
||||
}
|
||||
|
||||
export interface VirtualFile {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface VirtualRename {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import {
|
||||
getDyadWriteTags,
|
||||
getDyadRenameTags,
|
||||
getDyadDeleteTags,
|
||||
processFullResponseActions,
|
||||
getDyadAddDependencyTags,
|
||||
} from "../ipc/processors/response_processor";
|
||||
getDyadDeleteTags,
|
||||
} from "../ipc/utils/dyad_tag_parser";
|
||||
|
||||
import { processFullResponseActions } from "../ipc/processors/response_processor";
|
||||
import {
|
||||
removeDyadTags,
|
||||
hasUnclosedDyadWrite,
|
||||
|
||||
@@ -198,9 +198,9 @@ const PreviewHeader = ({
|
||||
<DropdownMenuItem onClick={onClearSessionData}>
|
||||
<Trash2 size={16} />
|
||||
<div className="flex flex-col">
|
||||
<span>Clear Preview Data</span>
|
||||
<span>Clear Cache</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Clears cookies and local storage for the app preview
|
||||
Clears cookies and local storage and other app cache
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -40,7 +40,7 @@ import { Worker } from "worker_threads";
|
||||
import { createFromTemplate } from "./createFromTemplate";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
import { safeSend } from "../utils/safe_sender";
|
||||
import { normalizePath } from "../processors/normalizePath";
|
||||
import { normalizePath } from "../../../shared/normalizePath";
|
||||
|
||||
async function copyDir(
|
||||
source: string,
|
||||
|
||||
@@ -22,10 +22,7 @@ import { getDyadAppPath } from "../../paths/paths";
|
||||
import { readSettings } from "../../main/settings";
|
||||
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
|
||||
import { extractCodebase, readFileWithCache } from "../../utils/codebase";
|
||||
import {
|
||||
getDyadAddDependencyTags,
|
||||
processFullResponseActions,
|
||||
} from "../processors/response_processor";
|
||||
import { processFullResponseActions } from "../processors/response_processor";
|
||||
import { streamTestResponse } from "./testing_chat_handlers";
|
||||
import { getTestResponse } from "./testing_chat_handlers";
|
||||
import { getModelClient, ModelClient } from "../utils/get_model_client";
|
||||
@@ -51,7 +48,13 @@ import { safeSend } from "../utils/safe_sender";
|
||||
import { cleanFullResponse } from "../utils/cleanFullResponse";
|
||||
import { generateProblemReport } from "../processors/tsc";
|
||||
import { createProblemFixPrompt } from "@/shared/problem_prompt";
|
||||
import { AsyncVirtualFileSystem } from "@/utils/VirtualFilesystem";
|
||||
import { AsyncVirtualFileSystem } from "../../../shared/VirtualFilesystem";
|
||||
import {
|
||||
getDyadAddDependencyTags,
|
||||
getDyadWriteTags,
|
||||
getDyadDeleteTags,
|
||||
getDyadRenameTags,
|
||||
} from "../utils/dyad_tag_parser";
|
||||
import { fileExists } from "../utils/file_utils";
|
||||
|
||||
type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
|
||||
@@ -708,7 +711,14 @@ ${problemReport.problems
|
||||
readFile: (fileName: string) => readFileWithCache(fileName),
|
||||
},
|
||||
);
|
||||
virtualFileSystem.applyResponseChanges(fullResponse);
|
||||
const writeTags = getDyadWriteTags(fullResponse);
|
||||
const renameTags = getDyadRenameTags(fullResponse);
|
||||
const deletePaths = getDyadDeleteTags(fullResponse);
|
||||
virtualFileSystem.applyResponseChanges({
|
||||
deletePaths,
|
||||
renameTags,
|
||||
writeTags,
|
||||
});
|
||||
|
||||
const { formattedOutput: codebaseInfo, files } =
|
||||
await extractCodebase({
|
||||
|
||||
@@ -9,16 +9,16 @@ import { messages, chats } from "../../db/schema";
|
||||
import { desc, eq, and } from "drizzle-orm";
|
||||
import path from "node:path"; // Import path for basename
|
||||
// Import tag parsers
|
||||
import { processFullResponseActions } from "../processors/response_processor";
|
||||
import {
|
||||
getDyadAddDependencyTags,
|
||||
getDyadChatSummaryTag,
|
||||
getDyadWriteTags,
|
||||
getDyadRenameTags,
|
||||
getDyadDeleteTags,
|
||||
getDyadExecuteSqlTags,
|
||||
getDyadRenameTags,
|
||||
getDyadWriteTags,
|
||||
getDyadAddDependencyTags,
|
||||
getDyadChatSummaryTag,
|
||||
getDyadCommandTags,
|
||||
processFullResponseActions,
|
||||
} from "../processors/response_processor";
|
||||
} from "../utils/dyad_tag_parser";
|
||||
import log from "electron-log";
|
||||
import { isServerFunction } from "../../supabase_admin/supabase_utils";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ipcMain, session } from "electron";
|
||||
import fs from "node:fs/promises";
|
||||
import { getTypeScriptCachePath } from "@/paths/paths";
|
||||
|
||||
export const registerSessionHandlers = () => {
|
||||
ipcMain.handle("clear-session-data", async (_event) => {
|
||||
@@ -8,5 +10,12 @@ export const registerSessionHandlers = () => {
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
console.info(`[IPC] All session data cleared for default session`);
|
||||
|
||||
// Clear custom cache data (like tsbuildinfo)
|
||||
try {
|
||||
await fs.rm(getTypeScriptCachePath(), { recursive: true, force: true });
|
||||
} catch {
|
||||
// Directory might not exist
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import type { ProblemReport, Problem } from "../../shared/tsc_types";
|
||||
export type { ProblemReport, Problem };
|
||||
|
||||
export interface AppOutput {
|
||||
type: "stdout" | "stderr" | "info" | "client-error";
|
||||
@@ -246,15 +248,3 @@ export interface AppUpgrade {
|
||||
manualUpgradeUrl: string;
|
||||
isNeeded: boolean;
|
||||
}
|
||||
|
||||
export interface Problem {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface ProblemReport {
|
||||
problems: Problem[];
|
||||
}
|
||||
|
||||
@@ -15,149 +15,21 @@ import {
|
||||
executeSupabaseSql,
|
||||
} from "../../supabase_admin/supabase_management_client";
|
||||
import { isServerFunction } from "../../supabase_admin/supabase_utils";
|
||||
import { SqlQuery, UserSettings } from "../../lib/schemas";
|
||||
import { UserSettings } from "../../lib/schemas";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
import { readSettings } from "@/main/settings";
|
||||
import { writeMigrationFile } from "../utils/file_utils";
|
||||
import { normalizePath } from "./normalizePath";
|
||||
import {
|
||||
getDyadWriteTags,
|
||||
getDyadRenameTags,
|
||||
getDyadDeleteTags,
|
||||
getDyadAddDependencyTags,
|
||||
getDyadExecuteSqlTags,
|
||||
} from "../utils/dyad_tag_parser";
|
||||
|
||||
const readFile = fs.promises.readFile;
|
||||
const logger = log.scope("response_processor");
|
||||
|
||||
export function getDyadWriteTags(fullResponse: string): {
|
||||
path: string;
|
||||
content: string;
|
||||
description?: string;
|
||||
}[] {
|
||||
const dyadWriteRegex = /<dyad-write([^>]*)>([\s\S]*?)<\/dyad-write>/gi;
|
||||
const pathRegex = /path="([^"]+)"/;
|
||||
const descriptionRegex = /description="([^"]+)"/;
|
||||
|
||||
let match;
|
||||
const tags: { path: string; content: string; description?: string }[] = [];
|
||||
|
||||
while ((match = dyadWriteRegex.exec(fullResponse)) !== null) {
|
||||
const attributesString = match[1];
|
||||
let content = match[2].trim();
|
||||
|
||||
const pathMatch = pathRegex.exec(attributesString);
|
||||
const descriptionMatch = descriptionRegex.exec(attributesString);
|
||||
|
||||
if (pathMatch && pathMatch[1]) {
|
||||
const path = pathMatch[1];
|
||||
const description = descriptionMatch?.[1];
|
||||
|
||||
const contentLines = content.split("\n");
|
||||
if (contentLines[0]?.startsWith("```")) {
|
||||
contentLines.shift();
|
||||
}
|
||||
if (contentLines[contentLines.length - 1]?.startsWith("```")) {
|
||||
contentLines.pop();
|
||||
}
|
||||
content = contentLines.join("\n");
|
||||
|
||||
tags.push({ path: normalizePath(path), content, description });
|
||||
} else {
|
||||
logger.warn(
|
||||
"Found <dyad-write> tag without a valid 'path' attribute:",
|
||||
match[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function getDyadRenameTags(fullResponse: string): {
|
||||
from: string;
|
||||
to: string;
|
||||
}[] {
|
||||
const dyadRenameRegex =
|
||||
/<dyad-rename from="([^"]+)" to="([^"]+)"[^>]*>([\s\S]*?)<\/dyad-rename>/g;
|
||||
let match;
|
||||
const tags: { from: string; to: string }[] = [];
|
||||
while ((match = dyadRenameRegex.exec(fullResponse)) !== null) {
|
||||
tags.push({
|
||||
from: normalizePath(match[1]),
|
||||
to: normalizePath(match[2]),
|
||||
});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function getDyadDeleteTags(fullResponse: string): string[] {
|
||||
const dyadDeleteRegex =
|
||||
/<dyad-delete path="([^"]+)"[^>]*>([\s\S]*?)<\/dyad-delete>/g;
|
||||
let match;
|
||||
const paths: string[] = [];
|
||||
while ((match = dyadDeleteRegex.exec(fullResponse)) !== null) {
|
||||
paths.push(normalizePath(match[1]));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function getDyadAddDependencyTags(fullResponse: string): string[] {
|
||||
const dyadAddDependencyRegex =
|
||||
/<dyad-add-dependency packages="([^"]+)">[^<]*<\/dyad-add-dependency>/g;
|
||||
let match;
|
||||
const packages: string[] = [];
|
||||
while ((match = dyadAddDependencyRegex.exec(fullResponse)) !== null) {
|
||||
packages.push(...match[1].split(" "));
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
export function getDyadChatSummaryTag(fullResponse: string): string | null {
|
||||
const dyadChatSummaryRegex =
|
||||
/<dyad-chat-summary>([\s\S]*?)<\/dyad-chat-summary>/g;
|
||||
const match = dyadChatSummaryRegex.exec(fullResponse);
|
||||
if (match && match[1]) {
|
||||
return match[1].trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDyadExecuteSqlTags(fullResponse: string): SqlQuery[] {
|
||||
const dyadExecuteSqlRegex =
|
||||
/<dyad-execute-sql([^>]*)>([\s\S]*?)<\/dyad-execute-sql>/g;
|
||||
const descriptionRegex = /description="([^"]+)"/;
|
||||
let match;
|
||||
const queries: { content: string; description?: string }[] = [];
|
||||
|
||||
while ((match = dyadExecuteSqlRegex.exec(fullResponse)) !== null) {
|
||||
const attributesString = match[1] || "";
|
||||
let content = match[2].trim();
|
||||
const descriptionMatch = descriptionRegex.exec(attributesString);
|
||||
const description = descriptionMatch?.[1];
|
||||
|
||||
// Handle markdown code blocks if present
|
||||
const contentLines = content.split("\n");
|
||||
if (contentLines[0]?.startsWith("```")) {
|
||||
contentLines.shift();
|
||||
}
|
||||
if (contentLines[contentLines.length - 1]?.startsWith("```")) {
|
||||
contentLines.pop();
|
||||
}
|
||||
content = contentLines.join("\n");
|
||||
|
||||
queries.push({ content, description });
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
export function getDyadCommandTags(fullResponse: string): string[] {
|
||||
const dyadCommandRegex =
|
||||
/<dyad-command type="([^"]+)"[^>]*><\/dyad-command>/g;
|
||||
let match;
|
||||
const commands: string[] = [];
|
||||
|
||||
while ((match = dyadCommandRegex.exec(fullResponse)) !== null) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
interface Output {
|
||||
message: string;
|
||||
error: unknown;
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { Worker } from "node:worker_threads";
|
||||
|
||||
import { ProblemReport } from "../ipc_types";
|
||||
import { Problem } from "../ipc_types";
|
||||
|
||||
import { normalizePath } from "./normalizePath";
|
||||
import { SyncVirtualFileSystem } from "../../utils/VirtualFilesystem";
|
||||
import log from "electron-log";
|
||||
import { WorkerInput, WorkerOutput } from "../../../shared/tsc_types";
|
||||
|
||||
import {
|
||||
getDyadDeleteTags,
|
||||
getDyadRenameTags,
|
||||
getDyadWriteTags,
|
||||
} from "../utils/dyad_tag_parser";
|
||||
import { getTypeScriptCachePath } from "@/paths/paths";
|
||||
|
||||
const logger = log.scope("tsc");
|
||||
|
||||
function loadLocalTypeScript(appPath: string): typeof import("typescript") {
|
||||
try {
|
||||
// Try to load TypeScript from the project's node_modules
|
||||
const requirePath = require.resolve("typescript", { paths: [appPath] });
|
||||
logger.info(`Loading TypeScript from ${requirePath} for app ${appPath}`);
|
||||
const ts = require(requirePath);
|
||||
return ts;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load TypeScript from ${appPath} because of ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateProblemReport({
|
||||
fullResponse,
|
||||
appPath,
|
||||
@@ -31,181 +21,61 @@ export async function generateProblemReport({
|
||||
fullResponse: string;
|
||||
appPath: string;
|
||||
}): Promise<ProblemReport> {
|
||||
// Load the local TypeScript version from the app's node_modules
|
||||
const ts = loadLocalTypeScript(appPath);
|
||||
return new Promise((resolve, reject) => {
|
||||
// Determine the worker script path
|
||||
const workerPath = path.join(__dirname, "tsc_worker.js");
|
||||
|
||||
// Create virtual file system with TypeScript system delegate and apply changes from response
|
||||
const vfs = new SyncVirtualFileSystem(appPath, {
|
||||
fileExists: (fileName: string) => ts.sys.fileExists(fileName),
|
||||
readFile: (fileName: string) => ts.sys.readFile(fileName),
|
||||
});
|
||||
vfs.applyResponseChanges(fullResponse);
|
||||
logger.info(`Starting TSC worker for app ${appPath}`);
|
||||
|
||||
// Find TypeScript config - throw error if not found
|
||||
const tsconfigPath = findTypeScriptConfig(appPath);
|
||||
// Create the worker
|
||||
const worker = new Worker(workerPath);
|
||||
|
||||
// Create TypeScript program with virtual file system
|
||||
const result = await runTypeScriptCheck(ts, appPath, tsconfigPath, vfs);
|
||||
return result;
|
||||
// Handle worker messages
|
||||
worker.on("message", (output: WorkerOutput) => {
|
||||
worker.terminate();
|
||||
|
||||
if (output.success && output.data) {
|
||||
logger.info(`TSC worker completed successfully for app ${appPath}`);
|
||||
resolve(output.data);
|
||||
} else {
|
||||
logger.error(`TSC worker failed for app ${appPath}: ${output.error}`);
|
||||
reject(new Error(output.error || "Unknown worker error"));
|
||||
}
|
||||
|
||||
function findTypeScriptConfig(appPath: string): string {
|
||||
const possibleConfigs = [
|
||||
// For vite applications, we want to check tsconfig.app.json, since it's the
|
||||
// most important one (client-side app).
|
||||
// The tsconfig.json in vite apps is a project reference and doesn't
|
||||
// actually check anything unless you do "--build" which requires a complex
|
||||
// programmatic approach
|
||||
"tsconfig.app.json",
|
||||
// For Next.js applications, it typically has a single tsconfig.json file
|
||||
"tsconfig.json",
|
||||
];
|
||||
|
||||
for (const config of possibleConfigs) {
|
||||
const configPath = path.join(appPath, config);
|
||||
if (fs.existsSync(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No TypeScript configuration file found in ${appPath}. Expected one of: ${possibleConfigs.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runTypeScriptCheck(
|
||||
ts: typeof import("typescript"),
|
||||
appPath: string,
|
||||
tsconfigPath: string,
|
||||
vfs: SyncVirtualFileSystem,
|
||||
): Promise<ProblemReport> {
|
||||
return runSingleProject(ts, appPath, tsconfigPath, vfs);
|
||||
}
|
||||
|
||||
async function runSingleProject(
|
||||
ts: typeof import("typescript"),
|
||||
appPath: string,
|
||||
tsconfigPath: string,
|
||||
vfs: SyncVirtualFileSystem,
|
||||
): Promise<ProblemReport> {
|
||||
// Use the idiomatic way to parse TypeScript config
|
||||
const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
|
||||
tsconfigPath,
|
||||
undefined, // No additional options
|
||||
{
|
||||
// Custom system object that can handle our virtual files
|
||||
...ts.sys,
|
||||
fileExists: (fileName: string) => vfs.fileExists(fileName),
|
||||
readFile: (fileName: string) => vfs.readFile(fileName),
|
||||
onUnRecoverableConfigFileDiagnostic: (
|
||||
diagnostic: import("typescript").Diagnostic,
|
||||
) => {
|
||||
throw new Error(
|
||||
`TypeScript config error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!parsedCommandLine) {
|
||||
throw new Error(`Failed to parse TypeScript config: ${tsconfigPath}`);
|
||||
}
|
||||
|
||||
let rootNames = parsedCommandLine.fileNames;
|
||||
|
||||
// Add any virtual files that aren't already included
|
||||
const virtualTsFiles = vfs
|
||||
.getVirtualFiles()
|
||||
.map((file) => path.resolve(appPath, file.path))
|
||||
.filter(isTypeScriptFile);
|
||||
|
||||
// Remove deleted files from rootNames
|
||||
const deletedFiles = vfs
|
||||
.getDeletedFiles()
|
||||
.map((file) => path.resolve(appPath, file));
|
||||
rootNames = rootNames.filter((fileName) => {
|
||||
const resolvedPath = path.resolve(fileName);
|
||||
return !deletedFiles.includes(resolvedPath);
|
||||
});
|
||||
|
||||
for (const virtualFile of virtualTsFiles) {
|
||||
if (!rootNames.includes(virtualFile)) {
|
||||
rootNames.push(virtualFile);
|
||||
}
|
||||
}
|
||||
// Handle worker errors
|
||||
worker.on("error", (error) => {
|
||||
logger.error(`TSC worker error for app ${appPath}:`, error);
|
||||
worker.terminate();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Create custom compiler host
|
||||
const host = createVirtualCompilerHost(
|
||||
ts,
|
||||
// Handle worker exit
|
||||
worker.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
logger.error(`TSC worker exited with code ${code} for app ${appPath}`);
|
||||
reject(new Error(`Worker exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
const writeTags = getDyadWriteTags(fullResponse);
|
||||
const renameTags = getDyadRenameTags(fullResponse);
|
||||
const deletePaths = getDyadDeleteTags(fullResponse);
|
||||
const virtualChanges = {
|
||||
deletePaths,
|
||||
renameTags,
|
||||
writeTags,
|
||||
};
|
||||
|
||||
// Send input to worker
|
||||
const input: WorkerInput = {
|
||||
virtualChanges,
|
||||
appPath,
|
||||
vfs,
|
||||
parsedCommandLine.options,
|
||||
);
|
||||
tsBuildInfoCacheDir: getTypeScriptCachePath(),
|
||||
};
|
||||
|
||||
// Create TypeScript program - this is the idiomatic way
|
||||
const program = ts.createProgram(rootNames, parsedCommandLine.options, host);
|
||||
logger.info(`Sending input to TSC worker for app ${appPath}`);
|
||||
|
||||
// Get diagnostics
|
||||
const diagnostics = [
|
||||
...program.getSyntacticDiagnostics(),
|
||||
...program.getSemanticDiagnostics(),
|
||||
...program.getGlobalDiagnostics(),
|
||||
];
|
||||
|
||||
// Convert diagnostics to our format
|
||||
const problems: Problem[] = [];
|
||||
|
||||
for (const diagnostic of diagnostics) {
|
||||
if (!diagnostic.file) continue;
|
||||
|
||||
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
||||
diagnostic.start!,
|
||||
);
|
||||
const message = ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
"\n",
|
||||
);
|
||||
|
||||
if (diagnostic.category !== ts.DiagnosticCategory.Error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
problems.push({
|
||||
file: normalizePath(path.relative(appPath, diagnostic.file.fileName)),
|
||||
line: line + 1, // Convert to 1-based
|
||||
column: character + 1, // Convert to 1-based
|
||||
message,
|
||||
code: diagnostic.code,
|
||||
worker.postMessage(input);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
problems,
|
||||
};
|
||||
}
|
||||
|
||||
function createVirtualCompilerHost(
|
||||
ts: typeof import("typescript"),
|
||||
appPath: string,
|
||||
vfs: SyncVirtualFileSystem,
|
||||
compilerOptions: import("typescript").CompilerOptions,
|
||||
): import("typescript").CompilerHost {
|
||||
const host = ts.createCompilerHost(compilerOptions);
|
||||
|
||||
// Override file reading to use virtual files
|
||||
host.readFile = (fileName: string) => {
|
||||
return vfs.readFile(fileName);
|
||||
};
|
||||
|
||||
// Override file existence check
|
||||
host.fileExists = (fileName: string) => {
|
||||
return vfs.fileExists(fileName);
|
||||
};
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
function isTypeScriptFile(fileName: string): boolean {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
|
||||
}
|
||||
|
||||
139
src/ipc/utils/dyad_tag_parser.ts
Normal file
139
src/ipc/utils/dyad_tag_parser.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { normalizePath } from "../../../shared/normalizePath";
|
||||
import log from "electron-log";
|
||||
import { SqlQuery } from "../../lib/schemas";
|
||||
|
||||
const logger = log.scope("dyad_tag_parser");
|
||||
|
||||
export function getDyadWriteTags(fullResponse: string): {
|
||||
path: string;
|
||||
content: string;
|
||||
description?: string;
|
||||
}[] {
|
||||
const dyadWriteRegex = /<dyad-write([^>]*)>([\s\S]*?)<\/dyad-write>/gi;
|
||||
const pathRegex = /path="([^"]+)"/;
|
||||
const descriptionRegex = /description="([^"]+)"/;
|
||||
|
||||
let match;
|
||||
const tags: { path: string; content: string; description?: string }[] = [];
|
||||
|
||||
while ((match = dyadWriteRegex.exec(fullResponse)) !== null) {
|
||||
const attributesString = match[1];
|
||||
let content = match[2].trim();
|
||||
|
||||
const pathMatch = pathRegex.exec(attributesString);
|
||||
const descriptionMatch = descriptionRegex.exec(attributesString);
|
||||
|
||||
if (pathMatch && pathMatch[1]) {
|
||||
const path = pathMatch[1];
|
||||
const description = descriptionMatch?.[1];
|
||||
|
||||
const contentLines = content.split("\n");
|
||||
if (contentLines[0]?.startsWith("```")) {
|
||||
contentLines.shift();
|
||||
}
|
||||
if (contentLines[contentLines.length - 1]?.startsWith("```")) {
|
||||
contentLines.pop();
|
||||
}
|
||||
content = contentLines.join("\n");
|
||||
|
||||
tags.push({ path: normalizePath(path), content, description });
|
||||
} else {
|
||||
logger.warn(
|
||||
"Found <dyad-write> tag without a valid 'path' attribute:",
|
||||
match[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function getDyadRenameTags(fullResponse: string): {
|
||||
from: string;
|
||||
to: string;
|
||||
}[] {
|
||||
const dyadRenameRegex =
|
||||
/<dyad-rename from="([^"]+)" to="([^"]+)"[^>]*>([\s\S]*?)<\/dyad-rename>/g;
|
||||
let match;
|
||||
const tags: { from: string; to: string }[] = [];
|
||||
while ((match = dyadRenameRegex.exec(fullResponse)) !== null) {
|
||||
tags.push({
|
||||
from: normalizePath(match[1]),
|
||||
to: normalizePath(match[2]),
|
||||
});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function getDyadDeleteTags(fullResponse: string): string[] {
|
||||
const dyadDeleteRegex =
|
||||
/<dyad-delete path="([^"]+)"[^>]*>([\s\S]*?)<\/dyad-delete>/g;
|
||||
let match;
|
||||
const paths: string[] = [];
|
||||
while ((match = dyadDeleteRegex.exec(fullResponse)) !== null) {
|
||||
paths.push(normalizePath(match[1]));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function getDyadAddDependencyTags(fullResponse: string): string[] {
|
||||
const dyadAddDependencyRegex =
|
||||
/<dyad-add-dependency packages="([^"]+)">[^<]*<\/dyad-add-dependency>/g;
|
||||
let match;
|
||||
const packages: string[] = [];
|
||||
while ((match = dyadAddDependencyRegex.exec(fullResponse)) !== null) {
|
||||
packages.push(...match[1].split(" "));
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
export function getDyadChatSummaryTag(fullResponse: string): string | null {
|
||||
const dyadChatSummaryRegex =
|
||||
/<dyad-chat-summary>([\s\S]*?)<\/dyad-chat-summary>/g;
|
||||
const match = dyadChatSummaryRegex.exec(fullResponse);
|
||||
if (match && match[1]) {
|
||||
return match[1].trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDyadExecuteSqlTags(fullResponse: string): SqlQuery[] {
|
||||
const dyadExecuteSqlRegex =
|
||||
/<dyad-execute-sql([^>]*)>([\s\S]*?)<\/dyad-execute-sql>/g;
|
||||
const descriptionRegex = /description="([^"]+)"/;
|
||||
let match;
|
||||
const queries: { content: string; description?: string }[] = [];
|
||||
|
||||
while ((match = dyadExecuteSqlRegex.exec(fullResponse)) !== null) {
|
||||
const attributesString = match[1] || "";
|
||||
let content = match[2].trim();
|
||||
const descriptionMatch = descriptionRegex.exec(attributesString);
|
||||
const description = descriptionMatch?.[1];
|
||||
|
||||
// Handle markdown code blocks if present
|
||||
const contentLines = content.split("\n");
|
||||
if (contentLines[0]?.startsWith("```")) {
|
||||
contentLines.shift();
|
||||
}
|
||||
if (contentLines[contentLines.length - 1]?.startsWith("```")) {
|
||||
contentLines.pop();
|
||||
}
|
||||
content = contentLines.join("\n");
|
||||
|
||||
queries.push({ content, description });
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
export function getDyadCommandTags(fullResponse: string): string[] {
|
||||
const dyadCommandRegex =
|
||||
/<dyad-command type="([^"]+)"[^>]*><\/dyad-command>/g;
|
||||
let match;
|
||||
const commands: string[] = [];
|
||||
|
||||
while ((match = dyadCommandRegex.exec(fullResponse)) !== null) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
@@ -193,11 +193,6 @@ export interface FileChange {
|
||||
isServerFunction: boolean;
|
||||
}
|
||||
|
||||
export interface SqlQuery {
|
||||
content: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CodeProposal {
|
||||
type: "code-proposal";
|
||||
title: string;
|
||||
@@ -268,3 +263,8 @@ export interface ProposalResult {
|
||||
chatId: number;
|
||||
messageId: number;
|
||||
}
|
||||
|
||||
export interface SqlQuery {
|
||||
content: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ export function getDyadAppPath(appPath: string): string {
|
||||
return path.join(os.homedir(), "dyad-apps", appPath);
|
||||
}
|
||||
|
||||
export function getTypeScriptCachePath(): string {
|
||||
const electron = getElectron();
|
||||
return path.join(electron!.app.getPath("sessionData"), "typescript-cache");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user data path, handling both Electron and non-Electron environments
|
||||
* In Electron: returns the app's userData directory
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
|
||||
import { glob } from "glob";
|
||||
import { AppChatContext } from "../lib/schemas";
|
||||
import { readSettings } from "@/main/settings";
|
||||
import { AsyncVirtualFileSystem } from "./VirtualFilesystem";
|
||||
import { AsyncVirtualFileSystem } from "../../shared/VirtualFilesystem";
|
||||
|
||||
const logger = log.scope("utils/codebase");
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "e2e-tests"],
|
||||
"include": ["src", "e2e-tests", "shared"],
|
||||
"exclude": ["e2e-tests/fixtures"]
|
||||
}
|
||||
|
||||
29
vite.worker.config.mts
Normal file
29
vite.worker.config.mts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
// target: "node16",
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "workers/tsc/tsc_worker.ts"),
|
||||
name: "tsc_worker",
|
||||
fileName: "tsc_worker",
|
||||
formats: ["cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["node:fs", "node:path", "node:worker_threads", "typescript"],
|
||||
// output: {
|
||||
// dir: "dist/workers/tsc",
|
||||
// },
|
||||
},
|
||||
// outDir: "dist/workers/tsc",
|
||||
// emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
307
workers/tsc/tsc_worker.ts
Normal file
307
workers/tsc/tsc_worker.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { parentPort } from "node:worker_threads";
|
||||
|
||||
import {
|
||||
Problem,
|
||||
ProblemReport,
|
||||
SyncVirtualFileSystem,
|
||||
WorkerInput,
|
||||
WorkerOutput,
|
||||
} from "../../shared/tsc_types";
|
||||
import { SyncVirtualFileSystemImpl } from "../../shared/VirtualFilesystem";
|
||||
|
||||
function loadLocalTypeScript(appPath: string): typeof import("typescript") {
|
||||
try {
|
||||
// Try to load TypeScript from the project's node_modules
|
||||
const requirePath = require.resolve("typescript", { paths: [appPath] });
|
||||
const ts = require(requirePath);
|
||||
return ts;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load TypeScript from ${appPath} because of ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findTypeScriptConfig(appPath: string): string {
|
||||
const possibleConfigs = [
|
||||
// For vite applications, we want to check tsconfig.app.json, since it's the
|
||||
// most important one (client-side app).
|
||||
// The tsconfig.json in vite apps is a project reference and doesn't
|
||||
// actually check anything unless you do "--build" which requires a complex
|
||||
// programmatic approach
|
||||
"tsconfig.app.json",
|
||||
// For Next.js applications, it typically has a single tsconfig.json file
|
||||
"tsconfig.json",
|
||||
];
|
||||
|
||||
for (const config of possibleConfigs) {
|
||||
const configPath = path.join(appPath, config);
|
||||
if (fs.existsSync(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No TypeScript configuration file found in ${appPath}. Expected one of: ${possibleConfigs.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runTypeScriptCheck(
|
||||
ts: typeof import("typescript"),
|
||||
vfs: SyncVirtualFileSystem,
|
||||
{
|
||||
appPath,
|
||||
tsconfigPath,
|
||||
tsBuildInfoCacheDir,
|
||||
}: {
|
||||
appPath: string;
|
||||
tsconfigPath: string;
|
||||
tsBuildInfoCacheDir: string;
|
||||
},
|
||||
): Promise<ProblemReport> {
|
||||
return runSingleProject(ts, vfs, {
|
||||
appPath,
|
||||
tsconfigPath,
|
||||
tsBuildInfoCacheDir,
|
||||
});
|
||||
}
|
||||
|
||||
async function runSingleProject(
|
||||
ts: typeof import("typescript"),
|
||||
vfs: SyncVirtualFileSystem,
|
||||
{
|
||||
appPath,
|
||||
tsconfigPath,
|
||||
tsBuildInfoCacheDir,
|
||||
}: {
|
||||
appPath: string;
|
||||
tsconfigPath: string;
|
||||
tsBuildInfoCacheDir: string;
|
||||
},
|
||||
): Promise<ProblemReport> {
|
||||
// Use the idiomatic way to parse TypeScript config
|
||||
const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
|
||||
tsconfigPath,
|
||||
undefined, // No additional options
|
||||
{
|
||||
// Custom system object that can handle our virtual files
|
||||
...ts.sys,
|
||||
fileExists: (fileName: string) => vfs.fileExists(fileName),
|
||||
readFile: (fileName: string) => vfs.readFile(fileName),
|
||||
onUnRecoverableConfigFileDiagnostic: (
|
||||
diagnostic: import("typescript").Diagnostic,
|
||||
) => {
|
||||
throw new Error(
|
||||
`TypeScript config error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!parsedCommandLine) {
|
||||
throw new Error(`Failed to parse TypeScript config: ${tsconfigPath}`);
|
||||
}
|
||||
|
||||
// Enable incremental compilation by setting tsBuildInfoFile if not already set
|
||||
const options = { ...parsedCommandLine.options };
|
||||
if (!options.tsBuildInfoFile && options.incremental !== false) {
|
||||
// Place the buildinfo file in a temp directory to avoid polluting the project
|
||||
const tmpDir = tsBuildInfoCacheDir;
|
||||
if (!fs.existsSync(tmpDir)) {
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a unique filename based on both the app path and tsconfig path to prevent collisions
|
||||
const configName = path.basename(tsconfigPath, path.extname(tsconfigPath));
|
||||
const appHash = Buffer.from(appPath)
|
||||
.toString("base64")
|
||||
.replace(/[/+=]/g, "_");
|
||||
options.tsBuildInfoFile = path.join(
|
||||
tmpDir,
|
||||
`${appHash}-${configName}.tsbuildinfo`,
|
||||
);
|
||||
options.incremental = true;
|
||||
}
|
||||
|
||||
let rootNames = parsedCommandLine.fileNames;
|
||||
|
||||
// Add any virtual files that aren't already included
|
||||
const virtualTsFiles = vfs
|
||||
.getVirtualFiles()
|
||||
.map((file) => path.resolve(appPath, file.path))
|
||||
.filter(isTypeScriptFile);
|
||||
|
||||
// Remove deleted files from rootNames
|
||||
const deletedFiles = vfs
|
||||
.getDeletedFiles()
|
||||
.map((file) => path.resolve(appPath, file));
|
||||
rootNames = rootNames.filter((fileName) => {
|
||||
const resolvedPath = path.resolve(fileName);
|
||||
return !deletedFiles.includes(resolvedPath);
|
||||
});
|
||||
|
||||
for (const virtualFile of virtualTsFiles) {
|
||||
if (!rootNames.includes(virtualFile)) {
|
||||
rootNames.push(virtualFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom compiler host
|
||||
const host = createVirtualCompilerHost(ts, appPath, vfs, options);
|
||||
|
||||
// Create incremental program - TypeScript will automatically use the tsBuildInfo file
|
||||
const builderProgram = ts.createIncrementalProgram({
|
||||
rootNames,
|
||||
options,
|
||||
host,
|
||||
configFileParsingDiagnostics:
|
||||
ts.getConfigFileParsingDiagnostics(parsedCommandLine),
|
||||
});
|
||||
|
||||
// Get diagnostics - the incremental program optimizes this by only checking changed files
|
||||
const diagnostics = [
|
||||
...builderProgram.getSyntacticDiagnostics(),
|
||||
...builderProgram.getSemanticDiagnostics(),
|
||||
...builderProgram.getGlobalDiagnostics(),
|
||||
];
|
||||
|
||||
// Emit the build info file to persist the incremental state
|
||||
builderProgram.emit();
|
||||
|
||||
// Convert diagnostics to our format
|
||||
const problems: Problem[] = [];
|
||||
|
||||
for (const diagnostic of diagnostics) {
|
||||
if (!diagnostic.file) continue;
|
||||
|
||||
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
||||
diagnostic.start!,
|
||||
);
|
||||
const message = ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
"\n",
|
||||
);
|
||||
|
||||
if (diagnostic.category !== ts.DiagnosticCategory.Error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
problems.push({
|
||||
file: normalizePath(path.relative(appPath, diagnostic.file.fileName)),
|
||||
line: line + 1, // Convert to 1-based
|
||||
column: character + 1, // Convert to 1-based
|
||||
message,
|
||||
code: diagnostic.code,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
problems,
|
||||
};
|
||||
}
|
||||
|
||||
function createVirtualCompilerHost(
|
||||
ts: typeof import("typescript"),
|
||||
appPath: string,
|
||||
vfs: SyncVirtualFileSystem,
|
||||
compilerOptions: import("typescript").CompilerOptions,
|
||||
): import("typescript").CompilerHost {
|
||||
const host = ts.createIncrementalCompilerHost(compilerOptions);
|
||||
|
||||
// Override file reading to use virtual files
|
||||
host.readFile = (fileName: string) => {
|
||||
return vfs.readFile(fileName);
|
||||
};
|
||||
|
||||
// Override file existence check
|
||||
host.fileExists = (fileName: string) => {
|
||||
return vfs.fileExists(fileName);
|
||||
};
|
||||
|
||||
// Override getCurrentDirectory to ensure proper resolution
|
||||
host.getCurrentDirectory = () => appPath;
|
||||
|
||||
// Override writeFile to handle virtual file system
|
||||
// This is important for writing the tsBuildInfo file
|
||||
const originalWriteFile = host.writeFile;
|
||||
host.writeFile = (
|
||||
fileName: string,
|
||||
data: string,
|
||||
writeByteOrderMark?: boolean,
|
||||
onError?: (message: string) => void,
|
||||
) => {
|
||||
// Only write build info files to disk, not emit files
|
||||
if (fileName.endsWith(".tsbuildinfo")) {
|
||||
originalWriteFile?.call(
|
||||
host,
|
||||
fileName,
|
||||
data,
|
||||
!!writeByteOrderMark,
|
||||
onError,
|
||||
);
|
||||
}
|
||||
// Ignore other emit files since we're only doing type checking
|
||||
};
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
function isTypeScriptFile(fileName: string): boolean {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
|
||||
}
|
||||
|
||||
async function processTypeScriptCheck(
|
||||
input: WorkerInput,
|
||||
): Promise<WorkerOutput> {
|
||||
try {
|
||||
const { appPath, virtualChanges, tsBuildInfoCacheDir } = input;
|
||||
|
||||
// Load the local TypeScript version from the app's node_modules
|
||||
const ts = loadLocalTypeScript(appPath);
|
||||
|
||||
const vfs = new SyncVirtualFileSystemImpl(appPath, {
|
||||
fileExists: (fileName: string) => ts.sys.fileExists(fileName),
|
||||
readFile: (fileName: string) => ts.sys.readFile(fileName),
|
||||
});
|
||||
vfs.applyResponseChanges(virtualChanges);
|
||||
|
||||
// Find TypeScript config - throw error if not found
|
||||
const tsconfigPath = findTypeScriptConfig(appPath);
|
||||
|
||||
// Create TypeScript program with virtual file system
|
||||
const result = await runTypeScriptCheck(ts, vfs, {
|
||||
appPath,
|
||||
tsconfigPath,
|
||||
tsBuildInfoCacheDir,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle messages from main thread
|
||||
parentPort?.on("message", async (input: WorkerInput) => {
|
||||
const output = await processTypeScriptCheck(input);
|
||||
parentPort?.postMessage(output);
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize the path to use forward slashes instead of backslashes.
|
||||
* This is important to prevent weird Git issues, particularly on Windows.
|
||||
* @param path Source path.
|
||||
* @returns Normalized path.
|
||||
*/
|
||||
function normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, "/");
|
||||
}
|
||||
20
workers/tsc/tsconfig.json
Normal file
20
workers/tsc/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "System",
|
||||
"lib": ["ES2022"],
|
||||
"outFile": "./dist/tsc_worker.js",
|
||||
"rootDir": "../../",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user