From e759a99231f38da3049daa5728b52fe95ae53735 Mon Sep 17 00:00:00 2001 From: Liam Wynne Date: Wed, 10 Jun 2026 14:14:49 +1000 Subject: [PATCH] fix: force-rename legacy erd-studio/ data dir to .erd-studio on activation v0.6.44 changed the default data directory to .erd-studio/ but left existing projects with an erd-studio/ folder looking empty (tree, model resolution, watchers all read the new default). Rename the folder in place at activation, before any service reads from disk, so existing projects keep working with no user action. Forced (not prompted) so collaborating teams converge: the first person to open the repo renames and commits; everyone else pulls the rename. Guards: skips when semanticDir is customised, when .erd-studio already exists, or when the folder doesn't look like an ERD data dir. Co-Authored-By: Claude Fable 5 --- src/extension.ts | 12 ++- src/services/migrationService.ts | 58 ++++++++++++++ test/unit/migrationService.test.ts | 124 +++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 test/unit/migrationService.test.ts 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); + }); + }); +});