diff --git a/cli/dev/DevServer.ts b/cli/dev/DevServer.ts
index 362e3f6..f8cc950 100644
--- a/cli/dev/DevServer.ts
+++ b/cli/dev/DevServer.ts
@@ -8,6 +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 { FilesystemTypesHandler } from "lib/dependency-analysis/FilesystemTypesHandler"
export class DevServer {
port: number
@@ -37,6 +38,8 @@ export class DevServer {
*/
filesystemWatcher?: chokidar.FSWatcher
+ private typesHandler?: FilesystemTypesHandler
+
constructor({
port,
componentFilePath,
@@ -50,6 +53,7 @@ export class DevServer {
this.fsKy = ky.create({
prefixUrl: `http://localhost:${port}`,
}) as any
+ this.typesHandler = new FilesystemTypesHandler(this.projectDir)
}
async start() {
@@ -77,6 +81,8 @@ export class DevServer {
)
this.upsertInitialFiles()
+
+ this.typesHandler?.handleInitialTypeDependencies(this.componentFilePath)
}
async addEntrypoint() {
@@ -119,6 +125,8 @@ circuit.add()
// because it can be edited by the browser
if (relativeFilePath.includes("manual-edits.json")) return
+ await this.typesHandler?.handleFileTypeDependencies(absoluteFilePath)
+
console.log(`${relativeFilePath} saved. Applying changes...`)
await this.fsKy
.post("api/files/upsert", {
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
+ }
+}
diff --git a/lib/dependency-analysis/findImportsInSnippet.ts b/lib/dependency-analysis/findImportsInSnippet.ts
new file mode 100644
index 0000000..93779e7
--- /dev/null
+++ b/lib/dependency-analysis/findImportsInSnippet.ts
@@ -0,0 +1,31 @@
+import * as fs from "node:fs"
+import * as ts from "typescript"
+
+export function findImportsInSnippet(snippetPath: string): string[] {
+ 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)
+
+ return imports
+}
diff --git a/tests/cli/dev/dev.test.ts b/tests/cli/dev/dev.test.ts
new file mode 100644
index 0000000..4d1e745
--- /dev/null
+++ b/tests/cli/dev/dev.test.ts
@@ -0,0 +1,65 @@
+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: {
+ "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()
+
+ // Wait for the initial type file to be installed
+ const typePath = path.join(
+ tempDirPath,
+ "node_modules/@tsci/seveibar.red-led/index.d.ts",
+ )
+ const typeFileExists = await waitForFile(typePath)
+ expect(typeFileExists).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)
+
+ // 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",
+ )
+ const typeFileExists2 = await waitForFile(typePath2)
+ expect(typeFileExists2).toBe(true)
+
+ // Stop the dev server after the test
+ await devServer.stop()
+}, 10_000)