From c5211e292105077c817645a73b497aab1acaf297 Mon Sep 17 00:00:00 2001 From: Douglas Gomes Date: Mon, 20 Apr 2026 13:45:49 -0300 Subject: [PATCH 1/5] fix: pass clickedUri to readConfig for correct multi-root workspace config resolution vscode.workspace.getConfiguration requires a resource URI (not the workspace folder root) to resolve per-folder settings. Passing the workspace root caused nameFormat and other template settings to be ignored in multi-root workspaces. Adds multi-root workspace tests to createComponent and updateComponent verifying that each folder's config is resolved independently based on the clicked path. Closes #246 Co-Authored-By: Claude Sonnet 4.6 --- .../commands/createComponent.test.ts | 93 ++++++++++++++++++ .../commands/updateComponent.test.ts | 95 +++++++++++++++++++ src/commands/createComponent.ts | 2 +- src/commands/updateComponent.ts | 2 +- 4 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/__tests__/commands/createComponent.test.ts b/src/__tests__/commands/createComponent.test.ts index b2ee9b1..5d79b6f 100644 --- a/src/__tests__/commands/createComponent.test.ts +++ b/src/__tests__/commands/createComponent.test.ts @@ -434,6 +434,99 @@ describe('createComponent', () => { }); }); + describe('multi-root workspace', () => { + const appClickedUri = vscode.Uri.file('/test/packages/app/src/components'); + const libClickedUri = vscode.Uri.file('/test/packages/lib/src/components'); + + beforeEach(() => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 }, + { uri: { fsPath: '/test/packages/lib' }, name: 'lib', index: 1 }, + ]; + + const configFactory = ( + _section?: string, + scope?: vscode.ConfigurationScope | null, + ): vscode.WorkspaceConfiguration => { + const fsPath = + scope != null && 'fsPath' in scope + ? (scope as vscode.Uri).fsPath + : undefined; + return { + get: jest.fn().mockImplementation((key: string) => { + if (key === 'templatesFolderPath') return 'templates'; + if (key === 'templateSettings') { + return fsPath?.startsWith('/test/packages/app') + ? { ReactPackage: { nameFormat: 'kebab-case' } } + : {}; + } + return undefined; + }), + update: jest.fn(), + has: jest.fn(), + inspect: jest.fn(), + } as unknown as vscode.WorkspaceConfiguration; + }; + + mockGetConfiguration.mockImplementation(configFactory); + }); + + afterEach(() => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/workspace' }, name: 'workspace', index: 0 }, + ]; + }); + + const setupTemplate = () => { + mockFs.readDirectory + .mockResolvedValueOnce([['ReactPackage', 2]] as never) + .mockResolvedValueOnce([['index.hbs', 1]] as never); + mockFs.readFile.mockResolvedValue(encode('// {{name}}') as never); + mockFs.stat.mockRejectedValue(new Error('not found')); + mockFs.createDirectory.mockResolvedValue(undefined); + mockFs.writeFile.mockResolvedValue(undefined); + }; + + it('applies nameFormat from the workspace folder containing the clicked path', async () => { + setupTemplate(); + mockWindow.showInputBox.mockResolvedValue('my-package'); + + await createComponent(appClickedUri); + + expect(mockWindow.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("'my-package' created"), + ); + }); + + it('applies a different config when clicking inside a different workspace folder', async () => { + setupTemplate(); + mockWindow.showInputBox.mockResolvedValue('my-package'); + + await createComponent(libClickedUri); + + // lib has no nameFormat → falls back to pascal-case + expect(mockWindow.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("'MyPackage' created"), + ); + }); + + it('passes clickedUri to getConfiguration, not the workspace folder root', async () => { + setupTemplate(); + mockWindow.showInputBox.mockResolvedValue('my-package'); + + await createComponent(appClickedUri); + + expect(mockGetConfiguration).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ fsPath: appClickedUri.fsPath }), + ); + }); + }); + describe('nameFormat per template', () => { const setupTemplateWithFormat = ( templateName: string, diff --git a/src/__tests__/commands/updateComponent.test.ts b/src/__tests__/commands/updateComponent.test.ts index dddd234..ce196a6 100644 --- a/src/__tests__/commands/updateComponent.test.ts +++ b/src/__tests__/commands/updateComponent.test.ts @@ -267,6 +267,101 @@ describe('updateComponent', () => { }); }); + describe('multi-root workspace', () => { + const appClickedUri = vscode.Uri.file( + '/test/packages/app/src/components/MyButton', + ); + const libClickedUri = vscode.Uri.file( + '/test/packages/lib/src/components/MyButton', + ); + + beforeEach(() => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 }, + { uri: { fsPath: '/test/packages/lib' }, name: 'lib', index: 1 }, + ]; + + const configFactory = ( + _section?: string, + scope?: vscode.ConfigurationScope | null, + ): vscode.WorkspaceConfiguration => { + const fsPath = + scope != null && 'fsPath' in scope + ? (scope as vscode.Uri).fsPath + : undefined; + return { + get: jest.fn().mockImplementation((key: string) => { + if (key === 'templatesFolderPath') return 'templates'; + if (key === 'templateSettings') { + return fsPath?.startsWith('/test/packages/app') + ? { ReactPackage: { nameFormat: 'kebab-case' } } + : {}; + } + return undefined; + }), + update: jest.fn(), + has: jest.fn(), + inspect: jest.fn(), + } as unknown as vscode.WorkspaceConfiguration; + }; + + mockGetConfiguration.mockImplementation(configFactory); + }); + + afterEach(() => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/workspace' }, name: 'workspace', index: 0 }, + ]; + }); + + const setupTemplate = () => { + mockFs.readDirectory + .mockResolvedValueOnce([['ReactPackage', 2]] as never) + .mockResolvedValueOnce([['index.hbs', 1]] as never); + mockFs.readFile.mockResolvedValue(encode('// {{name}}') as never); + mockFs.stat.mockRejectedValue(new Error('not found')); + mockFs.createDirectory.mockResolvedValue(undefined); + mockFs.writeFile.mockResolvedValue(undefined); + }; + + it('applies nameFormat from the workspace folder containing the clicked path', async () => { + setupTemplate(); + + await updateComponent(appClickedUri); + + // app has kebab-case → 'MyButton' becomes 'my-button' + expect(mockWindow.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("'my-button' updated"), + ); + }); + + it('applies a different config when clicking inside a different workspace folder', async () => { + setupTemplate(); + + await updateComponent(libClickedUri); + + // lib has no nameFormat → falls back to pascal-case → 'MyButton' stays + expect(mockWindow.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("'MyButton' updated"), + ); + }); + + it('passes clickedUri to getConfiguration, not the workspace folder root', async () => { + setupTemplate(); + + await updateComponent(appClickedUri); + + expect(mockGetConfiguration).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ fsPath: appClickedUri.fsPath }), + ); + }); + }); + describe('nameFormat per template', () => { const setupTemplateWithFormat = ( templateName: string, diff --git a/src/commands/createComponent.ts b/src/commands/createComponent.ts index 115415b..63ae8c1 100644 --- a/src/commands/createComponent.ts +++ b/src/commands/createComponent.ts @@ -39,7 +39,7 @@ export const createComponent = async ( return; } - const config = readConfig(workspaceFolderUri); + const config = readConfig(clickedUri); const templatesUri = vscode.Uri.joinPath( workspaceFolderUri, config.templatesFolderPath, diff --git a/src/commands/updateComponent.ts b/src/commands/updateComponent.ts index 5c17053..4fcf022 100644 --- a/src/commands/updateComponent.ts +++ b/src/commands/updateComponent.ts @@ -46,7 +46,7 @@ export const updateComponent = async ( return; } - const config = readConfig(workspaceFolderUri); + const config = readConfig(clickedUri); const templatesUri = vscode.Uri.joinPath( workspaceFolderUri, config.templatesFolderPath, From 11db3302c6ebc04d35ebbede39b1c599e491d545 Mon Sep 17 00:00:00 2001 From: Douglas Gomes Date: Mon, 20 Apr 2026 13:54:09 -0300 Subject: [PATCH 2/5] fix: use vscode.workspace.getWorkspaceFolder to correctly resolve templates folder The manual startsWith check in findWorkspaceFolderUri had a path-prefix collision bug: a click inside /packages/application would incorrectly match /packages/app, causing templates to be looked up in the wrong workspace folder. Replace the custom implementation with vscode.workspace.getWorkspaceFolder(uri) which handles path boundary matching correctly. Add getWorkspaceFolder to the vscode mock as a plain function (immune to jest.resetAllMocks) that reads workspaceFolders dynamically with proper path separator and specificity sorting. Adds regression tests for the path-prefix collision scenario. Co-Authored-By: Claude Sonnet 4.6 --- src/__mocks__/vscode.js | 14 ++++++++++ .../commands/createComponent.test.ts | 27 +++++++++++++++++++ .../commands/updateComponent.test.ts | 26 ++++++++++++++++++ src/commands/createComponent.ts | 13 ++------- src/commands/updateComponent.ts | 13 ++------- 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index c2d7033..58d5248 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -28,6 +28,20 @@ const workspace = { index: 0, }, ], + getWorkspaceFolder(uri) { + const folders = workspace.workspaceFolders; + if (!folders || folders.length === 0) return undefined; + const sep = require('node:path').sep; + return ( + [...folders] + .sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length) + .find( + f => + uri.fsPath === f.uri.fsPath || + uri.fsPath.startsWith(f.uri.fsPath + sep), + ) ?? undefined + ); + }, }; const window = { diff --git a/src/__tests__/commands/createComponent.test.ts b/src/__tests__/commands/createComponent.test.ts index 5d79b6f..a62399f 100644 --- a/src/__tests__/commands/createComponent.test.ts +++ b/src/__tests__/commands/createComponent.test.ts @@ -525,6 +525,33 @@ describe('createComponent', () => { expect.objectContaining({ fsPath: appClickedUri.fsPath }), ); }); + + it('does not confuse a folder whose name is a prefix of another folder', async () => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 }, + { + uri: { fsPath: '/test/packages/application' }, + name: 'application', + index: 1, + }, + ]; + + const applicationUri = vscode.Uri.file( + '/test/packages/application/src/components', + ); + setupTemplate(); + mockWindow.showInputBox.mockResolvedValue('my-package'); + + await createComponent(applicationUri); + + // templates must be looked up under /test/packages/application, not /test/packages/app + const templatesDirCall = mockFs.readDirectory.mock + .calls[0][0] as vscode.Uri; + expect(templatesDirCall.fsPath).toContain('/test/packages/application'); + expect(templatesDirCall.fsPath).not.toMatch(/\/test\/packages\/app\b/); + }); }); describe('nameFormat per template', () => { diff --git a/src/__tests__/commands/updateComponent.test.ts b/src/__tests__/commands/updateComponent.test.ts index ce196a6..170697a 100644 --- a/src/__tests__/commands/updateComponent.test.ts +++ b/src/__tests__/commands/updateComponent.test.ts @@ -360,6 +360,32 @@ describe('updateComponent', () => { expect.objectContaining({ fsPath: appClickedUri.fsPath }), ); }); + + it('does not confuse a folder whose name is a prefix of another folder', async () => { + ( + vscode.workspace as unknown as Record + ).workspaceFolders = [ + { uri: { fsPath: '/test/packages/app' }, name: 'app', index: 0 }, + { + uri: { fsPath: '/test/packages/application' }, + name: 'application', + index: 1, + }, + ]; + + const applicationUri = vscode.Uri.file( + '/test/packages/application/src/components/MyButton', + ); + setupTemplate(); + + await updateComponent(applicationUri); + + // templates must be looked up under /test/packages/application, not /test/packages/app + const templatesDirCall = mockFs.readDirectory.mock + .calls[0][0] as vscode.Uri; + expect(templatesDirCall.fsPath).toContain('/test/packages/application'); + expect(templatesDirCall.fsPath).not.toMatch(/\/test\/packages\/app\b/); + }); }); describe('nameFormat per template', () => { diff --git a/src/commands/createComponent.ts b/src/commands/createComponent.ts index 63ae8c1..830ed90 100644 --- a/src/commands/createComponent.ts +++ b/src/commands/createComponent.ts @@ -3,7 +3,6 @@ import * as vscode from 'vscode'; import { getNameFormat, readConfig } from '../helpers/config/readConfig'; import { listTemplates } from '../helpers/template/listTemplates'; import { processTemplateFolder } from '../helpers/template/processTemplateFolder'; -import { getWorkspaceFolders } from '../helpers/vscode/getWorkspaceFolders'; import { promptInput } from '../helpers/vscode/promptInput'; import { promptSelection } from '../helpers/vscode/promptSelection'; import { showError } from '../helpers/vscode/showError'; @@ -14,15 +13,6 @@ import { writeWorkspaceFile } from '../helpers/vscode/workspace/writeFile'; import { formatName } from '../utils/formatName'; import { COMPONENT_NAME_ERROR, canBeFormatted } from '../utils/validation'; -const findWorkspaceFolderUri = ( - clickedUri: vscode.Uri, -): vscode.Uri | undefined => { - const folders = getWorkspaceFolders(); - if (!folders) return undefined; - const match = folders.find(f => clickedUri.fsPath.startsWith(f.path)); - return match ? vscode.Uri.file(match.path) : undefined; -}; - export const createComponent = async ( clickedUri?: vscode.Uri, ): Promise => { @@ -31,7 +21,8 @@ export const createComponent = async ( return; } - const workspaceFolderUri = findWorkspaceFolderUri(clickedUri); + const workspaceFolderUri = + vscode.workspace.getWorkspaceFolder(clickedUri)?.uri; if (!workspaceFolderUri) { showError( 'Could not determine the workspace folder for the selected path.', diff --git a/src/commands/updateComponent.ts b/src/commands/updateComponent.ts index 4fcf022..b98fde3 100644 --- a/src/commands/updateComponent.ts +++ b/src/commands/updateComponent.ts @@ -3,7 +3,6 @@ import * as vscode from 'vscode'; import { getNameFormat, readConfig } from '../helpers/config/readConfig'; import { listTemplates } from '../helpers/template/listTemplates'; import { processTemplateFolder } from '../helpers/template/processTemplateFolder'; -import { getWorkspaceFolders } from '../helpers/vscode/getWorkspaceFolders'; import { promptSelection } from '../helpers/vscode/promptSelection'; import { showError } from '../helpers/vscode/showError'; import { showInfo } from '../helpers/vscode/showInfo'; @@ -13,15 +12,6 @@ import { writeWorkspaceFile } from '../helpers/vscode/workspace/writeFile'; import { formatName } from '../utils/formatName'; import { canBeFormatted } from '../utils/validation'; -const findWorkspaceFolderUri = ( - clickedUri: vscode.Uri, -): vscode.Uri | undefined => { - const folders = getWorkspaceFolders(); - if (!folders) return undefined; - const match = folders.find(f => clickedUri.fsPath.startsWith(f.path)); - return match ? vscode.Uri.file(match.path) : undefined; -}; - export const updateComponent = async ( clickedUri?: vscode.Uri, ): Promise => { @@ -38,7 +28,8 @@ export const updateComponent = async ( return; } - const workspaceFolderUri = findWorkspaceFolderUri(clickedUri); + const workspaceFolderUri = + vscode.workspace.getWorkspaceFolder(clickedUri)?.uri; if (!workspaceFolderUri) { showError( 'Could not determine the workspace folder for the selected path.', From 4ac3a645778c884acfbaca8518e5b7cbce8356eb Mon Sep 17 00:00:00 2001 From: Douglas Gomes Date: Mon, 20 Apr 2026 14:22:10 -0300 Subject: [PATCH 3/5] fix: declare configuration settings as resource-scoped Without scope: "resource", VS Code treats settings as window-scoped by default, which means .vscode/settings.json inside each workspace folder is silently ignored. Only .code-workspace file overrides were applied, so per-project templatesFolderPath and templateSettings were never picked up in multi-root workspaces. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index e8d7610..67ae903 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,13 @@ "codebot.templatesFolderPath": { "type": "string", "default": "templates", + "scope": "resource", "markdownDescription": "Path to the templates folder, relative to the workspace root." }, "codebot.templateSettings": { "type": "object", "default": {}, + "scope": "resource", "markdownDescription": "Per-template settings. Keys must match template folder names.", "additionalProperties": { "type": "object", From d70069033706e24f24cdaa97d1fcfbc02604cdb6 Mon Sep 17 00:00:00 2001 From: Douglas Gomes Date: Mon, 20 Apr 2026 14:36:48 -0300 Subject: [PATCH 4/5] chore: remove unused component and service templates Deleted .gitkeep files from components and services directories, along with their associated template files for component and service generation. This cleanup helps streamline the project structure by removing obsolete templates. --- .../project1/src/components/.gitkeep | 1 - .../templates/component/{{name}}.test.tsx.hbs | 9 ------- .../templates/component/{{name}}.tsx.hbs | 15 ------------ test-workspace/project2/src/services/.gitkeep | 1 - .../templates/service/{{name}}.service.ts.hbs | 24 ------------------- 5 files changed, 50 deletions(-) delete mode 100644 test-workspace/project1/src/components/.gitkeep delete mode 100644 test-workspace/project1/templates/component/{{name}}.test.tsx.hbs delete mode 100644 test-workspace/project1/templates/component/{{name}}.tsx.hbs delete mode 100644 test-workspace/project2/src/services/.gitkeep delete mode 100644 test-workspace/project2/templates/service/{{name}}.service.ts.hbs diff --git a/test-workspace/project1/src/components/.gitkeep b/test-workspace/project1/src/components/.gitkeep deleted file mode 100644 index 5d4e1ae..0000000 --- a/test-workspace/project1/src/components/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# This folder will contain generated components \ No newline at end of file diff --git a/test-workspace/project1/templates/component/{{name}}.test.tsx.hbs b/test-workspace/project1/templates/component/{{name}}.test.tsx.hbs deleted file mode 100644 index bc84a80..0000000 --- a/test-workspace/project1/templates/component/{{name}}.test.tsx.hbs +++ /dev/null @@ -1,9 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { {{name}} } from './{{name}}'; - -describe('{{name}}', () => { - it('should render correctly', () => { - render(<{{name}} />); - expect(screen.getByText('{{name}} Component')).toBeInTheDocument(); - }); -}); \ No newline at end of file diff --git a/test-workspace/project1/templates/component/{{name}}.tsx.hbs b/test-workspace/project1/templates/component/{{name}}.tsx.hbs deleted file mode 100644 index 1de3585..0000000 --- a/test-workspace/project1/templates/component/{{name}}.tsx.hbs +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -interface {{name}}Props { - // Add your props here -} - -export const {{name}}: React.FC<{{name}}Props> = () => { - return ( -
-

{{name}} Component

-
- ); -}; - -export default {{name}}; \ No newline at end of file diff --git a/test-workspace/project2/src/services/.gitkeep b/test-workspace/project2/src/services/.gitkeep deleted file mode 100644 index d30d0ab..0000000 --- a/test-workspace/project2/src/services/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# This folder will contain generated services \ No newline at end of file diff --git a/test-workspace/project2/templates/service/{{name}}.service.ts.hbs b/test-workspace/project2/templates/service/{{name}}.service.ts.hbs deleted file mode 100644 index 9eef259..0000000 --- a/test-workspace/project2/templates/service/{{name}}.service.ts.hbs +++ /dev/null @@ -1,24 +0,0 @@ -export class {{name}}Service { - constructor() { - // Initialize service - } - - async get{{name}}(): Promise { - // Implement your service logic here - return {}; - } - - async create{{name}}(data: any): Promise { - // Implement create logic - return data; - } - - async update{{name}}(id: string, data: any): Promise { - // Implement update logic - return { id, ...data }; - } - - async delete{{name}}(id: string): Promise { - // Implement delete logic - } -} \ No newline at end of file From d9e31a3fbf405189df6d28e3da053ac29f07bdb6 Mon Sep 17 00:00:00 2001 From: Douglas Gomes Date: Mon, 20 Apr 2026 14:41:20 -0300 Subject: [PATCH 5/5] chore: remove dead code - src/helpers/fs/ and its tests: all fs wrappers were unused in production - src/helpers/vscode/getWorkspaceFolders.ts and its test: removed from commands in the multi-root workspace refactor - src/helpers/index.ts: barrel file never imported anywhere - Remove getWorkspaceFolders/WorkspaceFolder exports from vscode/index.ts Co-Authored-By: Claude Sonnet 4.6 --- src/__tests__/helpers/fs/createFile.test.ts | 73 ------------------- src/__tests__/helpers/fs/createFolder.test.ts | 45 ------------ src/__tests__/helpers/fs/fileExists.test.ts | 32 -------- src/__tests__/helpers/fs/folderExists.test.ts | 30 -------- src/__tests__/helpers/fs/listFiles.test.ts | 56 -------------- src/__tests__/helpers/fs/readFile.test.ts | 43 ----------- .../vscode/getWorkspaceFolders.test.ts | 59 --------------- src/helpers/fs/createFile.ts | 23 ------ src/helpers/fs/createFolder.ts | 18 ----- src/helpers/fs/fileExists.ts | 10 --- src/helpers/fs/folderExists.ts | 9 --- src/helpers/fs/index.ts | 6 -- src/helpers/fs/listFiles.ts | 17 ----- src/helpers/fs/readFile.ts | 13 ---- src/helpers/index.ts | 3 - src/helpers/vscode/getWorkspaceFolders.ts | 18 ----- src/helpers/vscode/index.ts | 2 - 17 files changed, 457 deletions(-) delete mode 100644 src/__tests__/helpers/fs/createFile.test.ts delete mode 100644 src/__tests__/helpers/fs/createFolder.test.ts delete mode 100644 src/__tests__/helpers/fs/fileExists.test.ts delete mode 100644 src/__tests__/helpers/fs/folderExists.test.ts delete mode 100644 src/__tests__/helpers/fs/listFiles.test.ts delete mode 100644 src/__tests__/helpers/fs/readFile.test.ts delete mode 100644 src/__tests__/helpers/vscode/getWorkspaceFolders.test.ts delete mode 100644 src/helpers/fs/createFile.ts delete mode 100644 src/helpers/fs/createFolder.ts delete mode 100644 src/helpers/fs/fileExists.ts delete mode 100644 src/helpers/fs/folderExists.ts delete mode 100644 src/helpers/fs/index.ts delete mode 100644 src/helpers/fs/listFiles.ts delete mode 100644 src/helpers/fs/readFile.ts delete mode 100644 src/helpers/index.ts delete mode 100644 src/helpers/vscode/getWorkspaceFolders.ts diff --git a/src/__tests__/helpers/fs/createFile.test.ts b/src/__tests__/helpers/fs/createFile.test.ts deleted file mode 100644 index 5236116..0000000 --- a/src/__tests__/helpers/fs/createFile.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { createFile } from '../../../helpers/fs/createFile'; - -describe('createFile', () => { - afterEach(() => jest.restoreAllMocks()); - - it('creates parent directories and writes the file', () => { - const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); - const writeSpy = jest.spyOn(fs, 'writeFileSync').mockReturnValue(undefined); - - const result = createFile( - '/project/src/Button/Button.tsx', - 'export default function Button() {}', - ); - - expect(result).toEqual({ success: true, value: undefined }); - expect(mkdirSpy).toHaveBeenCalledWith( - path.dirname('/project/src/Button/Button.tsx'), - { - recursive: true, - }, - ); - expect(writeSpy).toHaveBeenCalledWith( - '/project/src/Button/Button.tsx', - 'export default function Button() {}', - 'utf-8', - ); - }); - - it('returns FILE_WRITE_ERROR when writeFileSync throws', () => { - jest.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); - jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { - throw new Error('Permission denied'); - }); - - const result = createFile('/locked/file.ts', 'content'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.code).toBe('FILE_WRITE_ERROR'); - expect(result.error.message).toContain('/locked/file.ts'); - } - }); - - it('returns FILE_WRITE_ERROR when mkdirSync throws', () => { - jest.spyOn(fs, 'mkdirSync').mockImplementation(() => { - throw new Error('Read-only filesystem'); - }); - - const result = createFile('/readonly/dir/file.ts', 'content'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.code).toBe('FILE_WRITE_ERROR'); - } - }); - - it('preserves the original error as cause', () => { - const originalError = new Error('EACCES'); - jest.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); - jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { - throw originalError; - }); - - const result = createFile('/file.ts', ''); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.cause).toBe(originalError); - } - }); -}); diff --git a/src/__tests__/helpers/fs/createFolder.test.ts b/src/__tests__/helpers/fs/createFolder.test.ts deleted file mode 100644 index 440c632..0000000 --- a/src/__tests__/helpers/fs/createFolder.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as fs from 'node:fs'; -import { createFolder } from '../../../helpers/fs/createFolder'; - -describe('createFolder', () => { - afterEach(() => jest.restoreAllMocks()); - - it('creates folder with recursive option', () => { - const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); - - const result = createFolder('/project/src/components/Button'); - - expect(result).toEqual({ success: true, value: undefined }); - expect(mkdirSpy).toHaveBeenCalledWith('/project/src/components/Button', { - recursive: true, - }); - }); - - it('returns FOLDER_CREATE_ERROR when mkdirSync throws', () => { - jest.spyOn(fs, 'mkdirSync').mockImplementation(() => { - throw new Error('EACCES: permission denied'); - }); - - const result = createFolder('/readonly/dir'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.code).toBe('FOLDER_CREATE_ERROR'); - expect(result.error.message).toContain('/readonly/dir'); - } - }); - - it('preserves the original error as cause', () => { - const originalError = new Error('EACCES'); - jest.spyOn(fs, 'mkdirSync').mockImplementation(() => { - throw originalError; - }); - - const result = createFolder('/dir'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.cause).toBe(originalError); - } - }); -}); diff --git a/src/__tests__/helpers/fs/fileExists.test.ts b/src/__tests__/helpers/fs/fileExists.test.ts deleted file mode 100644 index c495be3..0000000 --- a/src/__tests__/helpers/fs/fileExists.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as fs from 'node:fs'; -import { fileExists } from '../../../helpers/fs/fileExists'; - -describe('fileExists', () => { - afterEach(() => jest.restoreAllMocks()); - - it('returns true when file is accessible', () => { - jest.spyOn(fs, 'accessSync').mockReturnValue(undefined); - - expect(fileExists('/project/file.ts')).toBe(true); - expect(fs.accessSync).toHaveBeenCalledWith( - '/project/file.ts', - fs.constants.F_OK, - ); - }); - - it('returns false when file does not exist', () => { - jest.spyOn(fs, 'accessSync').mockImplementation(() => { - throw new Error('ENOENT'); - }); - - expect(fileExists('/missing/file.ts')).toBe(false); - }); - - it('returns false when access is denied', () => { - jest.spyOn(fs, 'accessSync').mockImplementation(() => { - throw new Error('EACCES'); - }); - - expect(fileExists('/protected/file.ts')).toBe(false); - }); -}); diff --git a/src/__tests__/helpers/fs/folderExists.test.ts b/src/__tests__/helpers/fs/folderExists.test.ts deleted file mode 100644 index 1b80aa6..0000000 --- a/src/__tests__/helpers/fs/folderExists.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as fs from 'node:fs'; -import { folderExists } from '../../../helpers/fs/folderExists'; - -describe('folderExists', () => { - afterEach(() => jest.restoreAllMocks()); - - it('returns true when path is a directory', () => { - jest - .spyOn(fs, 'statSync') - .mockReturnValue({ isDirectory: () => true } as fs.Stats); - - expect(folderExists('/project/src')).toBe(true); - }); - - it('returns false when path is a file, not a directory', () => { - jest - .spyOn(fs, 'statSync') - .mockReturnValue({ isDirectory: () => false } as fs.Stats); - - expect(folderExists('/project/file.ts')).toBe(false); - }); - - it('returns false when path does not exist', () => { - jest.spyOn(fs, 'statSync').mockImplementation(() => { - throw new Error('ENOENT'); - }); - - expect(folderExists('/missing/dir')).toBe(false); - }); -}); diff --git a/src/__tests__/helpers/fs/listFiles.test.ts b/src/__tests__/helpers/fs/listFiles.test.ts deleted file mode 100644 index 0cf38f8..0000000 --- a/src/__tests__/helpers/fs/listFiles.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fs from 'node:fs'; -import { listFiles } from '../../../helpers/fs/listFiles'; - -describe('listFiles', () => { - afterEach(() => jest.restoreAllMocks()); - - it('returns array of filenames on success', () => { - jest - .spyOn(fs, 'readdirSync') - .mockReturnValue(['Button.tsx', 'index.ts', 'Button.test.tsx'] as never); - - const result = listFiles('/project/src/Button'); - - expect(result).toEqual({ - success: true, - value: ['Button.tsx', 'index.ts', 'Button.test.tsx'], - }); - expect(fs.readdirSync).toHaveBeenCalledWith('/project/src/Button'); - }); - - it('returns empty array for empty directory', () => { - jest.spyOn(fs, 'readdirSync').mockReturnValue([] as never); - - const result = listFiles('/project/empty'); - - expect(result).toEqual({ success: true, value: [] }); - }); - - it('returns FOLDER_LIST_ERROR when directory does not exist', () => { - jest.spyOn(fs, 'readdirSync').mockImplementation(() => { - throw new Error('ENOENT: no such file or directory'); - }); - - const result = listFiles('/missing/dir'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.code).toBe('FOLDER_LIST_ERROR'); - expect(result.error.message).toContain('/missing/dir'); - } - }); - - it('preserves the original error as cause', () => { - const originalError = new Error('ENOENT'); - jest.spyOn(fs, 'readdirSync').mockImplementation(() => { - throw originalError; - }); - - const result = listFiles('/dir'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.cause).toBe(originalError); - } - }); -}); diff --git a/src/__tests__/helpers/fs/readFile.test.ts b/src/__tests__/helpers/fs/readFile.test.ts deleted file mode 100644 index f0a5f28..0000000 --- a/src/__tests__/helpers/fs/readFile.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as fs from 'node:fs'; -import { readFile } from '../../../helpers/fs/readFile'; - -describe('readFile', () => { - afterEach(() => jest.restoreAllMocks()); - - it('returns file content on success', () => { - jest.spyOn(fs, 'readFileSync').mockReturnValue('file content' as never); - - const result = readFile('/project/file.ts'); - - expect(result).toEqual({ success: true, value: 'file content' }); - expect(fs.readFileSync).toHaveBeenCalledWith('/project/file.ts', 'utf-8'); - }); - - it('returns FILE_READ_ERROR when file does not exist', () => { - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { - throw new Error('ENOENT: no such file or directory'); - }); - - const result = readFile('/missing/file.ts'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.code).toBe('FILE_READ_ERROR'); - expect(result.error.message).toContain('/missing/file.ts'); - } - }); - - it('preserves the original error as cause', () => { - const originalError = new Error('ENOENT'); - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { - throw originalError; - }); - - const result = readFile('/file.ts'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.cause).toBe(originalError); - } - }); -}); diff --git a/src/__tests__/helpers/vscode/getWorkspaceFolders.test.ts b/src/__tests__/helpers/vscode/getWorkspaceFolders.test.ts deleted file mode 100644 index 3810b8b..0000000 --- a/src/__tests__/helpers/vscode/getWorkspaceFolders.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as vscode from 'vscode'; -import { getWorkspaceFolders } from '../../../helpers/vscode/getWorkspaceFolders'; - -describe('getWorkspaceFolders', () => { - const originalFolders = vscode.workspace.workspaceFolders; - - afterEach(() => { - Object.defineProperty(vscode.workspace, 'workspaceFolders', { - value: originalFolders, - writable: true, - }); - }); - - it('returns mapped workspace folders when folders exist', () => { - Object.defineProperty(vscode.workspace, 'workspaceFolders', { - value: [ - { uri: { fsPath: '/project/root' }, name: 'root', index: 0 }, - { uri: { fsPath: '/project/packages/app' }, name: 'app', index: 1 }, - ], - writable: true, - }); - - const result = getWorkspaceFolders(); - - expect(result).toEqual([ - { name: 'root', path: '/project/root', index: 0 }, - { name: 'app', path: '/project/packages/app', index: 1 }, - ]); - }); - - it('returns undefined when workspaceFolders is undefined', () => { - Object.defineProperty(vscode.workspace, 'workspaceFolders', { - value: undefined, - writable: true, - }); - - expect(getWorkspaceFolders()).toBeUndefined(); - }); - - it('returns undefined when workspaceFolders is empty array', () => { - Object.defineProperty(vscode.workspace, 'workspaceFolders', { - value: [], - writable: true, - }); - - expect(getWorkspaceFolders()).toBeUndefined(); - }); - - it('maps fsPath to path field', () => { - Object.defineProperty(vscode.workspace, 'workspaceFolders', { - value: [{ uri: { fsPath: '/my/project' }, name: 'project', index: 0 }], - writable: true, - }); - - const result = getWorkspaceFolders(); - - expect(result?.[0].path).toBe('/my/project'); - }); -}); diff --git a/src/helpers/fs/createFile.ts b/src/helpers/fs/createFile.ts deleted file mode 100644 index 38289f1..0000000 --- a/src/helpers/fs/createFile.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { type AppError, makeError } from '../../types/AppError'; -import { err, ok, type Result } from '../../types/Result'; - -export const createFile = ( - filePath: string, - content: string, -): Result => { - try { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, content, 'utf-8'); - return ok(undefined); - } catch (cause) { - return err( - makeError( - 'FILE_WRITE_ERROR', - `Failed to create file: ${filePath}`, - cause, - ), - ); - } -}; diff --git a/src/helpers/fs/createFolder.ts b/src/helpers/fs/createFolder.ts deleted file mode 100644 index 5801fbd..0000000 --- a/src/helpers/fs/createFolder.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as fs from 'node:fs'; -import { type AppError, makeError } from '../../types/AppError'; -import { err, ok, type Result } from '../../types/Result'; - -export const createFolder = (folderPath: string): Result => { - try { - fs.mkdirSync(folderPath, { recursive: true }); - return ok(undefined); - } catch (cause) { - return err( - makeError( - 'FOLDER_CREATE_ERROR', - `Failed to create folder: ${folderPath}`, - cause, - ), - ); - } -}; diff --git a/src/helpers/fs/fileExists.ts b/src/helpers/fs/fileExists.ts deleted file mode 100644 index 2fac992..0000000 --- a/src/helpers/fs/fileExists.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as fs from 'node:fs'; - -export const fileExists = (filePath: string): boolean => { - try { - fs.accessSync(filePath, fs.constants.F_OK); - return true; - } catch { - return false; - } -}; diff --git a/src/helpers/fs/folderExists.ts b/src/helpers/fs/folderExists.ts deleted file mode 100644 index bd7b440..0000000 --- a/src/helpers/fs/folderExists.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as fs from 'node:fs'; - -export const folderExists = (folderPath: string): boolean => { - try { - return fs.statSync(folderPath).isDirectory(); - } catch { - return false; - } -}; diff --git a/src/helpers/fs/index.ts b/src/helpers/fs/index.ts deleted file mode 100644 index ab3d852..0000000 --- a/src/helpers/fs/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { createFile } from './createFile'; -export { createFolder } from './createFolder'; -export { fileExists } from './fileExists'; -export { folderExists } from './folderExists'; -export { listFiles } from './listFiles'; -export { readFile } from './readFile'; diff --git a/src/helpers/fs/listFiles.ts b/src/helpers/fs/listFiles.ts deleted file mode 100644 index f7983d4..0000000 --- a/src/helpers/fs/listFiles.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as fs from 'node:fs'; -import { type AppError, makeError } from '../../types/AppError'; -import { err, ok, type Result } from '../../types/Result'; - -export const listFiles = (folderPath: string): Result => { - try { - return ok(fs.readdirSync(folderPath)); - } catch (cause) { - return err( - makeError( - 'FOLDER_LIST_ERROR', - `Failed to list files in: ${folderPath}`, - cause, - ), - ); - } -}; diff --git a/src/helpers/fs/readFile.ts b/src/helpers/fs/readFile.ts deleted file mode 100644 index c20ee36..0000000 --- a/src/helpers/fs/readFile.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as fs from 'node:fs'; -import { type AppError, makeError } from '../../types/AppError'; -import { err, ok, type Result } from '../../types/Result'; - -export const readFile = (filePath: string): Result => { - try { - return ok(fs.readFileSync(filePath, 'utf-8')); - } catch (cause) { - return err( - makeError('FILE_READ_ERROR', `Failed to read file: ${filePath}`, cause), - ); - } -}; diff --git a/src/helpers/index.ts b/src/helpers/index.ts deleted file mode 100644 index 9559890..0000000 --- a/src/helpers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './fs'; -export * from './template'; -export * from './vscode'; diff --git a/src/helpers/vscode/getWorkspaceFolders.ts b/src/helpers/vscode/getWorkspaceFolders.ts deleted file mode 100644 index 7c86ffe..0000000 --- a/src/helpers/vscode/getWorkspaceFolders.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as vscode from 'vscode'; -import type { Option } from '../../types/Option'; - -export type WorkspaceFolder = { - readonly name: string; - readonly path: string; - readonly index: number; -}; - -export const getWorkspaceFolders = (): Option => { - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) return undefined; - return folders.map(folder => ({ - name: folder.name, - path: folder.uri.fsPath, - index: folder.index, - })); -}; diff --git a/src/helpers/vscode/index.ts b/src/helpers/vscode/index.ts index a268233..d9e0534 100644 --- a/src/helpers/vscode/index.ts +++ b/src/helpers/vscode/index.ts @@ -1,5 +1,3 @@ -export type { WorkspaceFolder } from './getWorkspaceFolders'; -export { getWorkspaceFolders } from './getWorkspaceFolders'; export type { InputOptions } from './promptInput'; export { promptInput } from './promptInput'; export type { SelectionOptions } from './promptSelection';