Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -186,6 +186,16 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const config = vscode.workspace.getConfiguration('dbtSemantic');
const semanticDir = config.get<string>('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);
Expand Down
58 changes: 58 additions & 0 deletions src/services/migrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
124 changes: 124 additions & 0 deletions test/unit/migrationService.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading