From b2387f3b9991cf9c784872b239bf9a6986ca9ef5 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Mon, 20 Jan 2025 20:11:39 +0000 Subject: [PATCH 01/16] feat: give indication that the server has refreshed after you saved --- cli/dev/DevServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 71ca1db..58193ed 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -115,11 +115,11 @@ circuit.add() async handleFileChangedOnFilesystem(absoluteFilePath: string) { const relativeFilePath = path.relative(this.projectDir, absoluteFilePath) - // We've temporarily disabled upserting manual edits from filesystem changes // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return + console.log(`${relativeFilePath} saved, server is refreshing...`) await this.fsKy .post("api/files/upsert", { json: { From eddd19188849d5ad48703ef2aa33cc6ee48c92be Mon Sep 17 00:00:00 2001 From: Arnav K Date: Mon, 20 Jan 2025 20:30:38 +0000 Subject: [PATCH 02/16] feat: import types on server refresh --- cli/dev/DevServer.ts | 14 +++++++ cli/dev/register.ts | 5 ++- .../check-if-file-imports-updated.ts | 38 +++++++++++++++++++ .../installNodeModuleTypesForSnippet.ts | 2 + 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 lib/dependency-analysis/check-if-file-imports-updated.ts diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 58193ed..0069498 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -8,6 +8,8 @@ import path from "node:path" import fs from "node:fs" import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent" import * as chokidar from "chokidar" +import { checkIfFileImportsUpdated } from "lib/dependency-analysis/check-if-file-imports-updated" +import { installNodeModuleTypesForSnippet } from "lib/dependency-analysis/installNodeModuleTypesForSnippet" export class DevServer { port: number @@ -18,6 +20,11 @@ export class DevServer { projectDir: string + /** + * List of imports whose type declarations are there + */ + imports: string[] + /** * The HTTP server that hosts the file server and event bus. You can use * fsKy to communicate with the file server/event bus @@ -40,11 +47,14 @@ export class DevServer { constructor({ port, componentFilePath, + imports, }: { port: number componentFilePath: string + imports: string[] }) { this.port = port + this.imports = imports this.componentFilePath = componentFilePath this.projectDir = path.dirname(componentFilePath) this.fsKy = ky.create({ @@ -120,6 +130,10 @@ circuit.add() if (relativeFilePath.includes("manual-edits.json")) return console.log(`${relativeFilePath} saved, server is refreshing...`) + if (await checkIfFileImportsUpdated(absoluteFilePath, this.imports)) { + await installNodeModuleTypesForSnippet(absoluteFilePath) + console.log("Types updated successfully") + } await this.fsKy .post("api/files/upsert", { json: { diff --git a/cli/dev/register.ts b/cli/dev/register.ts index f471f65..72e9eef 100644 --- a/cli/dev/register.ts +++ b/cli/dev/register.ts @@ -32,12 +32,12 @@ export const registerDev = (program: Command) => { return } } - const fileDir = path.dirname(absolutePath) + let imports: string[] = [] try { console.log("Installing types for imported snippets...") - await installNodeModuleTypesForSnippet(absolutePath) + imports = await installNodeModuleTypesForSnippet(absolutePath) console.log("Types installed successfully") } catch (error) { console.warn("Failed to install types:", error) @@ -46,6 +46,7 @@ export const registerDev = (program: Command) => { const server = new DevServer({ port, componentFilePath: absolutePath, + imports, }) await server.start() diff --git a/lib/dependency-analysis/check-if-file-imports-updated.ts b/lib/dependency-analysis/check-if-file-imports-updated.ts new file mode 100644 index 0000000..f06bf2b --- /dev/null +++ b/lib/dependency-analysis/check-if-file-imports-updated.ts @@ -0,0 +1,38 @@ +import * as fs from "node:fs" +import * as path from "node:path" +import * as ts from "typescript" + +export async function checkIfFileImportsUpdated( + snippetPath: string, + expectedImports: string[], +): Promise { + const content = fs.readFileSync(snippetPath, "utf-8") + const sourceFile = ts.createSourceFile( + snippetPath, + content, + ts.ScriptTarget.Latest, + true, + ) + + const imports: string[] = [] + + function visit(node: ts.Node) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier + if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) { + const importPath = moduleSpecifier.text + if (importPath.startsWith("@tsci/")) { + imports.push(importPath) + } + } + } + ts.forEachChild(node, visit) + } + + visit(sourceFile) + const allImportsPresent = expectedImports.every((item) => + imports.includes(item), + ) + + return allImportsPresent +} diff --git a/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts b/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts index 005e07e..848e7ca 100644 --- a/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts +++ b/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts @@ -71,4 +71,6 @@ export async function installNodeModuleTypesForSnippet(snippetPath: string) { console.warn(`Error fetching types for ${importPath}:`, error) } } + + return imports } From 4cd414be4ec1751c526d9c8d5c5fba2d3456532b Mon Sep 17 00:00:00 2001 From: Arnav K Date: Mon, 20 Jan 2025 20:33:13 +0000 Subject: [PATCH 03/16] chore: accidently placed code --- cli/dev/DevServer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 0069498..50169d3 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -129,7 +129,6 @@ circuit.add() // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return - console.log(`${relativeFilePath} saved, server is refreshing...`) if (await checkIfFileImportsUpdated(absoluteFilePath, this.imports)) { await installNodeModuleTypesForSnippet(absoluteFilePath) console.log("Types updated successfully") From cd8f483d3721a8db20735fb82a9b9a5aae2db9ff Mon Sep 17 00:00:00 2001 From: Arnav K Date: Tue, 21 Jan 2025 02:11:22 +0530 Subject: [PATCH 04/16] chore: dx message --- cli/dev/DevServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 50169d3..c88d432 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -130,8 +130,8 @@ circuit.add() if (relativeFilePath.includes("manual-edits.json")) return if (await checkIfFileImportsUpdated(absoluteFilePath, this.imports)) { + console.log("Types refreshing...") await installNodeModuleTypesForSnippet(absoluteFilePath) - console.log("Types updated successfully") } await this.fsKy .post("api/files/upsert", { From 79150d3a6ee62a5b5aa4e85d3ffb0575ccdc7149 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Tue, 21 Jan 2025 02:13:52 +0530 Subject: [PATCH 05/16] patch: optional prop import --- cli/dev/DevServer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index c88d432..739dca6 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -51,10 +51,10 @@ export class DevServer { }: { port: number componentFilePath: string - imports: string[] + imports?: string[] }) { this.port = port - this.imports = imports + this.imports = imports ?? [] this.componentFilePath = componentFilePath this.projectDir = path.dirname(componentFilePath) this.fsKy = ky.create({ From 279cc4e8b723cf7abe9c3987d35fd7c086109de7 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Tue, 21 Jan 2025 05:59:55 +0000 Subject: [PATCH 06/16] chore: update variable names --- cli/dev/DevServer.ts | 18 +++++++++++------- cli/dev/register.ts | 7 ++++--- ...mports-updated.ts => isFileImportsTyped.ts} | 18 +++++++++++------- 3 files changed, 26 insertions(+), 17 deletions(-) rename lib/dependency-analysis/{check-if-file-imports-updated.ts => isFileImportsTyped.ts} (73%) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 739dca6..234bb1a 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -8,7 +8,7 @@ import path from "node:path" import fs from "node:fs" import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent" import * as chokidar from "chokidar" -import { checkIfFileImportsUpdated } from "lib/dependency-analysis/check-if-file-imports-updated" +import { isFileImportsTyped } from "lib/dependency-analysis/isFileImportsTyped" import { installNodeModuleTypesForSnippet } from "lib/dependency-analysis/installNodeModuleTypesForSnippet" export class DevServer { @@ -23,7 +23,7 @@ export class DevServer { /** * List of imports whose type declarations are there */ - imports: string[] + resolvedTypedImports: string[] /** * The HTTP server that hosts the file server and event bus. You can use @@ -47,14 +47,14 @@ export class DevServer { constructor({ port, componentFilePath, - imports, + resolvedTypedImports, }: { port: number componentFilePath: string - imports?: string[] + resolvedTypedImports?: string[] }) { this.port = port - this.imports = imports ?? [] + this.resolvedTypedImports = resolvedTypedImports ?? [] this.componentFilePath = componentFilePath this.projectDir = path.dirname(componentFilePath) this.fsKy = ky.create({ @@ -129,9 +129,13 @@ circuit.add() // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return - if (await checkIfFileImportsUpdated(absoluteFilePath, this.imports)) { + if (!isFileImportsTyped(absoluteFilePath, this.resolvedTypedImports)) { console.log("Types refreshing...") - await installNodeModuleTypesForSnippet(absoluteFilePath) + const updatedImports = + await installNodeModuleTypesForSnippet(absoluteFilePath) + this.resolvedTypedImports = [ + ...new Set([...this.resolvedTypedImports, ...updatedImports]), + ] } await this.fsKy .post("api/files/upsert", { diff --git a/cli/dev/register.ts b/cli/dev/register.ts index 72e9eef..c796521 100644 --- a/cli/dev/register.ts +++ b/cli/dev/register.ts @@ -33,11 +33,12 @@ export const registerDev = (program: Command) => { } } const fileDir = path.dirname(absolutePath) - let imports: string[] = [] + let resolvedTypedImports: string[] = [] try { console.log("Installing types for imported snippets...") - imports = await installNodeModuleTypesForSnippet(absolutePath) + resolvedTypedImports = + await installNodeModuleTypesForSnippet(absolutePath) console.log("Types installed successfully") } catch (error) { console.warn("Failed to install types:", error) @@ -46,7 +47,7 @@ export const registerDev = (program: Command) => { const server = new DevServer({ port, componentFilePath: absolutePath, - imports, + resolvedTypedImports, }) await server.start() diff --git a/lib/dependency-analysis/check-if-file-imports-updated.ts b/lib/dependency-analysis/isFileImportsTyped.ts similarity index 73% rename from lib/dependency-analysis/check-if-file-imports-updated.ts rename to lib/dependency-analysis/isFileImportsTyped.ts index f06bf2b..d8fd39f 100644 --- a/lib/dependency-analysis/check-if-file-imports-updated.ts +++ b/lib/dependency-analysis/isFileImportsTyped.ts @@ -2,10 +2,10 @@ import * as fs from "node:fs" import * as path from "node:path" import * as ts from "typescript" -export async function checkIfFileImportsUpdated( +export function isFileImportsTyped( snippetPath: string, - expectedImports: string[], -): Promise { + typedImports: string[], +): boolean { const content = fs.readFileSync(snippetPath, "utf-8") const sourceFile = ts.createSourceFile( snippetPath, @@ -30,9 +30,13 @@ export async function checkIfFileImportsUpdated( } visit(sourceFile) - const allImportsPresent = expectedImports.every((item) => - imports.includes(item), + const result = imports.every((item) => typedImports.includes(item)) + console.log( + !result, + "\nEXPECTED:- ", + typedImports, + "\nFILE IMPORTS:-", + imports, ) - - return allImportsPresent + return result } From 8a8a7184f5840684bd5e605a6faee65505fb6f69 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Tue, 21 Jan 2025 11:33:54 +0530 Subject: [PATCH 07/16] chore: removed removed debug logs --- lib/dependency-analysis/isFileImportsTyped.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/dependency-analysis/isFileImportsTyped.ts b/lib/dependency-analysis/isFileImportsTyped.ts index d8fd39f..d88dfdd 100644 --- a/lib/dependency-analysis/isFileImportsTyped.ts +++ b/lib/dependency-analysis/isFileImportsTyped.ts @@ -30,13 +30,5 @@ export function isFileImportsTyped( } visit(sourceFile) - const result = imports.every((item) => typedImports.includes(item)) - console.log( - !result, - "\nEXPECTED:- ", - typedImports, - "\nFILE IMPORTS:-", - imports, - ) - return result + return imports.every((item) => typedImports.includes(item)) } From 29a21d5f16be0b06f2f5cdfaaa8e76dae323c537 Mon Sep 17 00:00:00 2001 From: Arnav K Date: Thu, 23 Jan 2025 18:29:18 +0000 Subject: [PATCH 08/16] patch: isFileImportsTyped -> isFileTypesImported --- cli/dev/DevServer.ts | 4 ++-- .../{isFileImportsTyped.ts => isFileTypesImported.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/dependency-analysis/{isFileImportsTyped.ts => isFileTypesImported.ts} (95%) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 79b57b4..62af049 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -8,7 +8,7 @@ import path from "node:path" import fs from "node:fs" import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent" import * as chokidar from "chokidar" -import { isFileImportsTyped } from "lib/dependency-analysis/isFileImportsTyped" +import { isFileTypesImported } from "lib/dependency-analysis/isFileTypesImported" import { installNodeModuleTypesForSnippet } from "lib/dependency-analysis/installNodeModuleTypesForSnippet" export class DevServer { @@ -129,7 +129,7 @@ circuit.add() // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return - if (!isFileImportsTyped(absoluteFilePath, this.resolvedTypedImports)) { + if (!isFileTypesImported(absoluteFilePath, this.resolvedTypedImports)) { console.log("Types refreshing...") const updatedImports = await installNodeModuleTypesForSnippet(absoluteFilePath) diff --git a/lib/dependency-analysis/isFileImportsTyped.ts b/lib/dependency-analysis/isFileTypesImported.ts similarity index 95% rename from lib/dependency-analysis/isFileImportsTyped.ts rename to lib/dependency-analysis/isFileTypesImported.ts index d88dfdd..0fe8847 100644 --- a/lib/dependency-analysis/isFileImportsTyped.ts +++ b/lib/dependency-analysis/isFileTypesImported.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs" import * as path from "node:path" import * as ts from "typescript" -export function isFileImportsTyped( +export function isFileTypesImported( snippetPath: string, typedImports: string[], ): boolean { From 66a9cd382dcd5b15128511d47e4cb60fd0df2751 Mon Sep 17 00:00:00 2001 From: 0039dope <0039dope@gmail.com> Date: Sun, 26 Jan 2025 19:08:50 +0000 Subject: [PATCH 09/16] better approach --- cli/dev/DevServer.ts | 67 ++++++++++++++----- ...pesImported.ts => findImportsInSnippet.ts} | 8 +-- 2 files changed, 54 insertions(+), 21 deletions(-) rename lib/dependency-analysis/{isFileTypesImported.ts => findImportsInSnippet.ts} (77%) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 62af049..b2d0596 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -8,8 +8,8 @@ import path from "node:path" import fs from "node:fs" import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent" import * as chokidar from "chokidar" -import { isFileTypesImported } from "lib/dependency-analysis/isFileTypesImported" import { installNodeModuleTypesForSnippet } from "lib/dependency-analysis/installNodeModuleTypesForSnippet" +import { findImportsInSnippet } from "lib/dependency-analysis/findImportsInSnippet" export class DevServer { port: number @@ -20,11 +20,6 @@ export class DevServer { projectDir: string - /** - * List of imports whose type declarations are there - */ - resolvedTypedImports: string[] - /** * The HTTP server that hosts the file server and event bus. You can use * fsKy to communicate with the file server/event bus @@ -47,14 +42,11 @@ export class DevServer { constructor({ port, componentFilePath, - resolvedTypedImports, }: { port: number componentFilePath: string - resolvedTypedImports?: string[] }) { this.port = port - this.resolvedTypedImports = resolvedTypedImports ?? [] this.componentFilePath = componentFilePath this.projectDir = path.dirname(componentFilePath) this.fsKy = ky.create({ @@ -87,6 +79,8 @@ export class DevServer { ) this.upsertInitialFiles() + + await this.handleTypeDependencies(this.componentFilePath) } async addEntrypoint() { @@ -129,13 +123,13 @@ circuit.add() // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return - if (!isFileTypesImported(absoluteFilePath, this.resolvedTypedImports)) { - console.log("Types refreshing...") - const updatedImports = + try { + if (!this.areTypesInstalled(absoluteFilePath)) { + console.log("Types outdated, installing...") await installNodeModuleTypesForSnippet(absoluteFilePath) - this.resolvedTypedImports = [ - ...new Set([...this.resolvedTypedImports, ...updatedImports]), - ] + } + } catch (error) { + console.warn("Failed to verify types:", error) } console.log(`${relativeFilePath} saved. Applying changes...`) @@ -174,4 +168,47 @@ circuit.add() this.httpServer?.close() this.eventsWatcher?.stop() } + + private async handleTypeDependencies(absoluteFilePath: string) { + console.log("Checking type dependencies...") + try { + const needsInstallation = !this.areTypesInstalled(absoluteFilePath) + if (needsInstallation) { + console.log("Installing missing types...") + await installNodeModuleTypesForSnippet(absoluteFilePath) + } + } catch (error) { + console.warn("Error handling type dependencies:", error) + } + } + + private areTypesInstalled(absoluteFilePath: string): boolean { + const imports = findImportsInSnippet(absoluteFilePath) + return imports.every((imp) => this.checkTypeExists(imp)) + } + + private checkTypeExists(importPath: string): boolean { + if (!importPath.startsWith("@tsci/")) return true + + let projectRoot = this.projectDir + while (projectRoot !== path.parse(projectRoot).root) { + if (fs.existsSync(path.join(projectRoot, "package.json"))) { + break + } + projectRoot = path.dirname(projectRoot) + } + + const pathWithoutPrefix = importPath.replace("@tsci/", "") + const [owner, name] = pathWithoutPrefix.split(".") + + const typePath = path.join( + projectRoot, + "node_modules", + "@tsci", + `${owner}.${name}`, + "index.d.ts", + ) + + return fs.existsSync(typePath) + } } diff --git a/lib/dependency-analysis/isFileTypesImported.ts b/lib/dependency-analysis/findImportsInSnippet.ts similarity index 77% rename from lib/dependency-analysis/isFileTypesImported.ts rename to lib/dependency-analysis/findImportsInSnippet.ts index 0fe8847..c517479 100644 --- a/lib/dependency-analysis/isFileTypesImported.ts +++ b/lib/dependency-analysis/findImportsInSnippet.ts @@ -1,11 +1,7 @@ import * as fs from "node:fs" -import * as path from "node:path" import * as ts from "typescript" -export function isFileTypesImported( - snippetPath: string, - typedImports: string[], -): boolean { +export function findImportsInSnippet(snippetPath: string): string[] { const content = fs.readFileSync(snippetPath, "utf-8") const sourceFile = ts.createSourceFile( snippetPath, @@ -30,5 +26,5 @@ export function isFileTypesImported( } visit(sourceFile) - return imports.every((item) => typedImports.includes(item)) + return imports } From da313fc5adc5f1e7e484e465102d3a7131f708d9 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Sun, 26 Jan 2025 19:15:39 +0000 Subject: [PATCH 10/16] fix types --- cli/dev/register.ts | 6 ++---- lib/dependency-analysis/findImportsInSnippet.ts | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/dev/register.ts b/cli/dev/register.ts index c796521..f471f65 100644 --- a/cli/dev/register.ts +++ b/cli/dev/register.ts @@ -32,13 +32,12 @@ export const registerDev = (program: Command) => { return } } + const fileDir = path.dirname(absolutePath) - let resolvedTypedImports: string[] = [] try { console.log("Installing types for imported snippets...") - resolvedTypedImports = - await installNodeModuleTypesForSnippet(absolutePath) + await installNodeModuleTypesForSnippet(absolutePath) console.log("Types installed successfully") } catch (error) { console.warn("Failed to install types:", error) @@ -47,7 +46,6 @@ export const registerDev = (program: Command) => { const server = new DevServer({ port, componentFilePath: absolutePath, - resolvedTypedImports, }) await server.start() diff --git a/lib/dependency-analysis/findImportsInSnippet.ts b/lib/dependency-analysis/findImportsInSnippet.ts index c517479..93779e7 100644 --- a/lib/dependency-analysis/findImportsInSnippet.ts +++ b/lib/dependency-analysis/findImportsInSnippet.ts @@ -26,5 +26,6 @@ export function findImportsInSnippet(snippetPath: string): string[] { } visit(sourceFile) + return imports } From 32cdd91ca9c3008f25478e909f66a45d008c102e Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Sun, 26 Jan 2025 19:20:18 +0000 Subject: [PATCH 11/16] revert: unused patch --- lib/dependency-analysis/installNodeModuleTypesForSnippet.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts b/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts index 848e7ca..005e07e 100644 --- a/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts +++ b/lib/dependency-analysis/installNodeModuleTypesForSnippet.ts @@ -71,6 +71,4 @@ export async function installNodeModuleTypesForSnippet(snippetPath: string) { console.warn(`Error fetching types for ${importPath}:`, error) } } - - return imports } From 7b866f6fc14fcda722bb777e7c7e85f5d9b7db8e Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Tue, 28 Jan 2025 13:49:29 +0000 Subject: [PATCH 12/16] implement filesystemtypeshandler --- cli/dev/DevServer.ts | 59 ++-------------- .../FilesystemTypesHandler.ts | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 lib/dependency-analysis/FilesystemTypesHandler.ts diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index b2d0596..88af477 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -8,8 +8,7 @@ import path from "node:path" import fs from "node:fs" import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent" import * as chokidar from "chokidar" -import { installNodeModuleTypesForSnippet } from "lib/dependency-analysis/installNodeModuleTypesForSnippet" -import { findImportsInSnippet } from "lib/dependency-analysis/findImportsInSnippet" +import { FilesystemTypesHandler } from "lib/dependency-analysis/FilesystemTypesHandler" export class DevServer { port: number @@ -39,6 +38,8 @@ export class DevServer { */ filesystemWatcher?: chokidar.FSWatcher + private typesHandler?: FilesystemTypesHandler + constructor({ port, componentFilePath, @@ -80,7 +81,7 @@ export class DevServer { this.upsertInitialFiles() - await this.handleTypeDependencies(this.componentFilePath) + this.typesHandler = new FilesystemTypesHandler(this.projectDir) } async addEntrypoint() { @@ -123,14 +124,7 @@ circuit.add() // because it can be edited by the browser if (relativeFilePath.includes("manual-edits.json")) return - try { - if (!this.areTypesInstalled(absoluteFilePath)) { - console.log("Types outdated, installing...") - await installNodeModuleTypesForSnippet(absoluteFilePath) - } - } catch (error) { - console.warn("Failed to verify types:", error) - } + await this.typesHandler?.handleFileTypeDependencies(absoluteFilePath) console.log(`${relativeFilePath} saved. Applying changes...`) await this.fsKy @@ -168,47 +162,4 @@ circuit.add() this.httpServer?.close() this.eventsWatcher?.stop() } - - private async handleTypeDependencies(absoluteFilePath: string) { - console.log("Checking type dependencies...") - try { - const needsInstallation = !this.areTypesInstalled(absoluteFilePath) - if (needsInstallation) { - console.log("Installing missing types...") - await installNodeModuleTypesForSnippet(absoluteFilePath) - } - } catch (error) { - console.warn("Error handling type dependencies:", error) - } - } - - private areTypesInstalled(absoluteFilePath: string): boolean { - const imports = findImportsInSnippet(absoluteFilePath) - return imports.every((imp) => this.checkTypeExists(imp)) - } - - private checkTypeExists(importPath: string): boolean { - if (!importPath.startsWith("@tsci/")) return true - - let projectRoot = this.projectDir - while (projectRoot !== path.parse(projectRoot).root) { - if (fs.existsSync(path.join(projectRoot, "package.json"))) { - break - } - projectRoot = path.dirname(projectRoot) - } - - const pathWithoutPrefix = importPath.replace("@tsci/", "") - const [owner, name] = pathWithoutPrefix.split(".") - - const typePath = path.join( - projectRoot, - "node_modules", - "@tsci", - `${owner}.${name}`, - "index.d.ts", - ) - - return fs.existsSync(typePath) - } } diff --git a/lib/dependency-analysis/FilesystemTypesHandler.ts b/lib/dependency-analysis/FilesystemTypesHandler.ts new file mode 100644 index 0000000..badf4dc --- /dev/null +++ b/lib/dependency-analysis/FilesystemTypesHandler.ts @@ -0,0 +1,68 @@ +import * as fs from "node:fs" +import * as path from "node:path" +import { findImportsInSnippet } from "./findImportsInSnippet" +import { installNodeModuleTypesForSnippet } from "./installNodeModuleTypesForSnippet" + +export class FilesystemTypesHandler { + private projectRoot: string + + constructor(initialDir: string) { + this.projectRoot = this.findProjectRoot(initialDir) + } + + async handleInitialTypeDependencies(filePath: string) { + console.log("Checking initial type dependencies...") + try { + if (!this.areTypesInstalled(filePath)) { + console.log("Installing missing initial types...") + await installNodeModuleTypesForSnippet(filePath) + } + } catch (error) { + console.warn("Error handling initial type dependencies:", error) + } + } + + async handleFileTypeDependencies(filePath: string) { + try { + if (!this.areTypesInstalled(filePath)) { + console.log("Installing missing file types...") + await installNodeModuleTypesForSnippet(filePath) + } + } catch (error) { + console.warn("Failed to verify types:", error) + } + } + + private areTypesInstalled(filePath: string): boolean { + const imports = findImportsInSnippet(filePath) + return imports.every((imp) => this.checkTypeExists(imp)) + } + + private checkTypeExists(importPath: string): boolean { + if (!importPath.startsWith("@tsci/")) return true + + const pathWithoutPrefix = importPath.replace("@tsci/", "") + const [owner, name] = pathWithoutPrefix.split(".") + + const typePath = path.join( + this.projectRoot, + "node_modules", + "@tsci", + `${owner}.${name}`, + "index.d.ts", + ) + + return fs.existsSync(typePath) + } + + private findProjectRoot(startDir: string): string { + let root = path.resolve(startDir) + while (root !== path.parse(root).root) { + if (fs.existsSync(path.join(root, "package.json"))) { + return root + } + root = path.dirname(root) + } + return startDir + } +} From 791bd03d7b822317b20994f1342d75f0a179a7de Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Tue, 28 Jan 2025 14:31:14 +0000 Subject: [PATCH 13/16] tests: invalid --- tests/cli/dev/dev.test.ts | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/cli/dev/dev.test.ts diff --git a/tests/cli/dev/dev.test.ts b/tests/cli/dev/dev.test.ts new file mode 100644 index 0000000..4cfb20d --- /dev/null +++ b/tests/cli/dev/dev.test.ts @@ -0,0 +1,50 @@ +import { test, expect, afterEach, beforeEach, mock } from "bun:test" +import { DevServer } from "cli/dev/DevServer" +import { getTestFixture } from "tests/fixtures/get-test-fixture" +import fs from "node:fs" +import path from "node:path" + +test.skip("types are installed and refreshed when files change", async () => { + const { tempDirPath, devServerPort } = await getTestFixture({ + vfs: { + "snippet.tsx": ` + import { useRedLed } from "@tsci/seveibar.red-led" + export const MyCircuit = () => <> + `, + "package.json": "{}", + }, + }) + + const devServer = new DevServer({ + port: devServerPort, + componentFilePath: `${tempDirPath}/snippet.tsx`, + }) + + await devServer.start() + + // Verify initial type installation + const typePath = path.join( + tempDirPath, + "node_modules/@tsci/seveibar.red-led/index.d.ts", + ) + expect(fs.existsSync(typePath)).toBe(true) + + // Simulate file change with new import + const updatedContent = ` + import { useUsbC } from "@tsci/seveibar.smd-usb-c" + export const MyCircuit = () => <> + ` + fs.writeFileSync(`${tempDirPath}/snippet.tsx`, updatedContent) + + // Trigger file change handler + await devServer.handleFileChangedOnFilesystem(`${tempDirPath}/snippet.tsx`) + + // Verify new types file still exists after update + const typePath2 = path.join( + tempDirPath, + "node_modules/@tsci/seveibar.smd-usb-c/index.d.ts", + ) + expect(fs.existsSync(typePath2)).toBe(true) + + devServer.stop() +}, 10_000) From cbb9378ed57ea4bf249e04886fc3f934a372e0da Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Tue, 28 Jan 2025 14:32:05 +0000 Subject: [PATCH 14/16] todo: fix test --- tests/cli/dev/dev.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/cli/dev/dev.test.ts b/tests/cli/dev/dev.test.ts index 4cfb20d..a2b3636 100644 --- a/tests/cli/dev/dev.test.ts +++ b/tests/cli/dev/dev.test.ts @@ -46,5 +46,7 @@ test.skip("types are installed and refreshed when files change", async () => { ) expect(fs.existsSync(typePath2)).toBe(true) - devServer.stop() + afterEach(async () => { + await devServer.stop() + }) }, 10_000) From 44d574f34b5332cf663df6136b8bfd07a90425d1 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Tue, 28 Jan 2025 15:00:22 +0000 Subject: [PATCH 15/16] test: fixed --- tests/cli/dev/dev.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cli/dev/dev.test.ts b/tests/cli/dev/dev.test.ts index a2b3636..7ef87a4 100644 --- a/tests/cli/dev/dev.test.ts +++ b/tests/cli/dev/dev.test.ts @@ -4,7 +4,7 @@ import { getTestFixture } from "tests/fixtures/get-test-fixture" import fs from "node:fs" import path from "node:path" -test.skip("types are installed and refreshed when files change", async () => { +test("types are installed and refreshed when files change", async () => { const { tempDirPath, devServerPort } = await getTestFixture({ vfs: { "snippet.tsx": ` @@ -21,6 +21,7 @@ test.skip("types are installed and refreshed when files change", async () => { }) await devServer.start() + await devServer.handleFileChangedOnFilesystem(`${tempDirPath}/snippet.tsx`) // Verify initial type installation const typePath = path.join( From 51a86347b181c80ed6f7db37a71d2d0ca6011de9 Mon Sep 17 00:00:00 2001 From: ArnavK-09 Date: Tue, 28 Jan 2025 17:12:10 +0000 Subject: [PATCH 16/16] tests: wait for node modules to load --- cli/dev/DevServer.ts | 3 ++- tests/cli/dev/dev.test.ts | 36 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts index 88af477..f8cc950 100644 --- a/cli/dev/DevServer.ts +++ b/cli/dev/DevServer.ts @@ -53,6 +53,7 @@ export class DevServer { this.fsKy = ky.create({ prefixUrl: `http://localhost:${port}`, }) as any + this.typesHandler = new FilesystemTypesHandler(this.projectDir) } async start() { @@ -81,7 +82,7 @@ export class DevServer { this.upsertInitialFiles() - this.typesHandler = new FilesystemTypesHandler(this.projectDir) + this.typesHandler?.handleInitialTypeDependencies(this.componentFilePath) } async addEntrypoint() { diff --git a/tests/cli/dev/dev.test.ts b/tests/cli/dev/dev.test.ts index 7ef87a4..4d1e745 100644 --- a/tests/cli/dev/dev.test.ts +++ b/tests/cli/dev/dev.test.ts @@ -1,9 +1,24 @@ -import { test, expect, afterEach, beforeEach, mock } from "bun:test" +import { test, expect, afterEach } from "bun:test" import { DevServer } from "cli/dev/DevServer" import { getTestFixture } from "tests/fixtures/get-test-fixture" import fs from "node:fs" import path from "node:path" +async function waitForFile( + filePath: string, + timeout: number = 5000, + interval: number = 500, +): Promise { + const endTime = Date.now() + timeout + while (Date.now() < endTime) { + if (fs.existsSync(filePath)) { + return true + } + await new Promise((resolve) => setTimeout(resolve, interval)) + } + return false +} + test("types are installed and refreshed when files change", async () => { const { tempDirPath, devServerPort } = await getTestFixture({ vfs: { @@ -21,14 +36,14 @@ test("types are installed and refreshed when files change", async () => { }) await devServer.start() - await devServer.handleFileChangedOnFilesystem(`${tempDirPath}/snippet.tsx`) - // Verify initial type installation + // Wait for the initial type file to be installed const typePath = path.join( tempDirPath, "node_modules/@tsci/seveibar.red-led/index.d.ts", ) - expect(fs.existsSync(typePath)).toBe(true) + const typeFileExists = await waitForFile(typePath) + expect(typeFileExists).toBe(true) // Simulate file change with new import const updatedContent = ` @@ -37,17 +52,14 @@ test("types are installed and refreshed when files change", async () => { ` fs.writeFileSync(`${tempDirPath}/snippet.tsx`, updatedContent) - // Trigger file change handler - await devServer.handleFileChangedOnFilesystem(`${tempDirPath}/snippet.tsx`) - - // Verify new types file still exists after update + // Wait for the new type file to be installed after update const typePath2 = path.join( tempDirPath, "node_modules/@tsci/seveibar.smd-usb-c/index.d.ts", ) - expect(fs.existsSync(typePath2)).toBe(true) + const typeFileExists2 = await waitForFile(typePath2) + expect(typeFileExists2).toBe(true) - afterEach(async () => { - await devServer.stop() - }) + // Stop the dev server after the test + await devServer.stop() }, 10_000)