diff --git a/src/extension.ts b/src/extension.ts index eb35936..6637a34 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,7 @@ import { HarnessService, HARNESS_TARGETS, HARNESS_VERSION } from './services/har import { SelectorsService } from './services/selectorsService'; import { LegacyTagCleanupService } from './services/legacyTagCleanupService'; import { LogicalModelService } from './services/logicalModelService'; -import { MigrationService } from './services/migrationService'; +import { MigrationService, migrateLegacySemanticDir } from './services/migrationService'; import { YmlParserService } from './services/ymlParserService'; import { ModelLibraryTreeProvider, type ModelLibraryNode } from './providers/ModelLibraryTreeProvider'; import { DOMAIN_EDITOR_VIEW_TYPE, hasOpenDomainCanvas, saveAllAndReload } from './services/recoveryService'; @@ -186,6 +186,16 @@ export async function activate(context: vscode.ExtensionContext): Promise const config = vscode.workspace.getConfiguration('dbtSemantic'); const semanticDir = config.get('semanticDir', '.erd-studio'); + // v0.6.44 moved the default data directory from erd-studio/ to .erd-studio/. + // Rename legacy folders in place before any service reads from disk so + // existing projects keep working without intervention. + if (migrateLegacySemanticDir(workspaceRoot, semanticDir)) { + void vscode.window.showInformationMessage( + 'ERD Studio: your erd-studio/ folder was renamed to .erd-studio/ (the new default location). ' + + 'Commit the rename so collaborators stay in sync.', + ); + } + const layerService = new LayerService(workspaceRoot, semanticDir); const domainService = new DomainService(layerService); const logicalModelService = new LogicalModelService(workspaceRoot, semanticDir); diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 5559a46..dca07b9 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -46,6 +46,64 @@ export interface MigrationResult { mergeConflicts: string[]; } +// --------------------------------------------------------------------------- +// Legacy directory migration (erd-studio/ → .erd-studio/) +// --------------------------------------------------------------------------- + +const DEFAULT_SEMANTIC_DIR = '.erd-studio'; +const LEGACY_SEMANTIC_DIR = 'erd-studio'; + +/** Files/dirs whose presence identifies a folder as an ERD Studio data dir. */ +const ERD_DIR_MARKERS = ['layers.json', 'logical-models', 'templates']; + +/** + * Returns the absolute path of a legacy erd-studio/ data directory that + * should be renamed to .erd-studio/, or null when no migration applies. + * + * Migration applies only when ALL of: + * - the effective semanticDir is the default '.erd-studio' (a custom + * setting means the user manages the location themselves) + * - '.erd-studio' does not already exist + * - 'erd-studio' exists, is a directory, and looks like an ERD data dir + * (has a known marker, or a layer subdirectory containing .json files) + */ +export function findLegacySemanticDir( + workspaceRoot: string, + semanticDir: string, +): string | null { + if (semanticDir !== DEFAULT_SEMANTIC_DIR) return null; + if (fs.existsSync(path.join(workspaceRoot, semanticDir))) return null; + + const legacy = path.join(workspaceRoot, LEGACY_SEMANTIC_DIR); + if (!fs.existsSync(legacy) || !fs.statSync(legacy).isDirectory()) return null; + + const hasMarker = ERD_DIR_MARKERS.some((m) => fs.existsSync(path.join(legacy, m))); + if (hasMarker) return legacy; + + const hasLayerWithDomains = fs.readdirSync(legacy).some((entry) => { + const sub = path.join(legacy, entry); + return ( + fs.statSync(sub).isDirectory() && + fs.readdirSync(sub).some((f) => f.endsWith('.json')) + ); + }); + return hasLayerWithDomains ? legacy : null; +} + +/** + * Rename a legacy erd-studio/ directory to .erd-studio/ in place. + * Returns true when a rename happened. + */ +export function migrateLegacySemanticDir( + workspaceRoot: string, + semanticDir: string, +): boolean { + const legacy = findLegacySemanticDir(workspaceRoot, semanticDir); + if (!legacy) return false; + fs.renameSync(legacy, path.join(workspaceRoot, semanticDir)); + return true; +} + // --------------------------------------------------------------------------- // Service // --------------------------------------------------------------------------- diff --git a/test/unit/migrationService.test.ts b/test/unit/migrationService.test.ts new file mode 100644 index 0000000..d306aaf --- /dev/null +++ b/test/unit/migrationService.test.ts @@ -0,0 +1,124 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + findLegacySemanticDir, + migrateLegacySemanticDir, +} from '../../src/services/migrationService'; + +describe('legacy semantic dir migration (erd-studio/ → .erd-studio/)', () => { + let workspaceRoot: string; + + beforeEach(() => { + workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'erd-migration-')); + }); + + afterEach(() => { + fs.rmSync(workspaceRoot, { recursive: true, force: true }); + }); + + function makeLegacyDir(contents: 'layers' | 'logical-models' | 'templates' | 'layer-domains' | 'empty' | 'unrelated') { + const legacy = path.join(workspaceRoot, 'erd-studio'); + fs.mkdirSync(legacy); + switch (contents) { + case 'layers': + fs.writeFileSync(path.join(legacy, 'layers.json'), '{"layers":[]}'); + break; + case 'logical-models': + fs.mkdirSync(path.join(legacy, 'logical-models')); + break; + case 'templates': + fs.mkdirSync(path.join(legacy, 'templates')); + break; + case 'layer-domains': + fs.mkdirSync(path.join(legacy, 'silver')); + fs.writeFileSync(path.join(legacy, 'silver', 'orders.json'), '{}'); + break; + case 'unrelated': + fs.writeFileSync(path.join(legacy, 'notes.txt'), 'not an erd dir'); + break; + case 'empty': + break; + } + return legacy; + } + + describe('findLegacySemanticDir', () => { + it.each(['layers', 'logical-models', 'templates', 'layer-domains'] as const)( + 'detects a legacy dir identified by %s', + (marker) => { + const legacy = makeLegacyDir(marker); + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBe(legacy); + }, + ); + + it('returns null when no legacy dir exists', () => { + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBeNull(); + }); + + it('returns null when .erd-studio already exists', () => { + makeLegacyDir('layers'); + fs.mkdirSync(path.join(workspaceRoot, '.erd-studio')); + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBeNull(); + }); + + it('returns null when semanticDir is customised', () => { + makeLegacyDir('layers'); + expect(findLegacySemanticDir(workspaceRoot, 'erd-studio')).toBeNull(); + expect(findLegacySemanticDir(workspaceRoot, 'custom/dir')).toBeNull(); + }); + + it('returns null for a folder that does not look like an ERD data dir', () => { + makeLegacyDir('unrelated'); + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBeNull(); + }); + + it('returns null for an empty erd-studio folder', () => { + makeLegacyDir('empty'); + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBeNull(); + }); + + it('returns null when erd-studio is a file, not a directory', () => { + fs.writeFileSync(path.join(workspaceRoot, 'erd-studio'), 'file'); + expect(findLegacySemanticDir(workspaceRoot, '.erd-studio')).toBeNull(); + }); + }); + + describe('migrateLegacySemanticDir', () => { + it('renames the legacy dir and preserves contents', () => { + makeLegacyDir('layer-domains'); + fs.writeFileSync( + path.join(workspaceRoot, 'erd-studio', 'layers.json'), + '{"layers":[]}', + ); + + const renamed = migrateLegacySemanticDir(workspaceRoot, '.erd-studio'); + + expect(renamed).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, 'erd-studio'))).toBe(false); + expect( + fs.readFileSync( + path.join(workspaceRoot, '.erd-studio', 'silver', 'orders.json'), + 'utf-8', + ), + ).toBe('{}'); + expect( + fs.existsSync(path.join(workspaceRoot, '.erd-studio', 'layers.json')), + ).toBe(true); + }); + + it('is a no-op when nothing to migrate', () => { + expect(migrateLegacySemanticDir(workspaceRoot, '.erd-studio')).toBe(false); + }); + + it('is a no-op when .erd-studio already exists alongside a legacy dir', () => { + makeLegacyDir('layers'); + fs.mkdirSync(path.join(workspaceRoot, '.erd-studio')); + + expect(migrateLegacySemanticDir(workspaceRoot, '.erd-studio')).toBe(false); + expect(fs.existsSync(path.join(workspaceRoot, 'erd-studio'))).toBe(true); + }); + }); +});