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
1 change: 0 additions & 1 deletion src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ declare global {
addLocalVersion(token: string, name: string): Array<Version>;
cancelPendingLocalVersion(token: string): void;
removeLocalVersion(version: string): Array<Version>;
migrateLocalVersions(versions: Version[]): boolean;
getNodeTypes(
version: string,
): Promise<{ version: string; types: NodeTypes } | undefined>;
Expand Down
2 changes: 0 additions & 2 deletions src/ipc-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export enum IpcEvents {
ADD_LOCAL_VERSION = 'ADD_LOCAL_VERSION',
REMOVE_LOCAL_VERSION = 'REMOVE_LOCAL_VERSION',
CANCEL_PENDING_LOCAL_VERSION = 'CANCEL_PENDING_LOCAL_VERSION',
MIGRATE_LOCAL_VERSIONS = 'MIGRATE_LOCAL_VERSIONS',
GET_OLDEST_SUPPORTED_MAJOR = 'GET_OLDEST_SUPPORTED_MAJOR',
GET_RELEASED_VERSIONS = 'GET_RELEASED_VERSIONS',
GET_RELEASE_INFO = 'GET_RELEASE_INFO',
Expand Down Expand Up @@ -106,7 +105,6 @@ export const ipcMainEvents = [
IpcEvents.ADD_LOCAL_VERSION,
IpcEvents.REMOVE_LOCAL_VERSION,
IpcEvents.CANCEL_PENDING_LOCAL_VERSION,
IpcEvents.MIGRATE_LOCAL_VERSIONS,
IpcEvents.GET_OLDEST_SUPPORTED_MAJOR,
IpcEvents.GET_RELEASED_VERSIONS,
IpcEvents.GET_USERNAME,
Expand Down
42 changes: 1 addition & 41 deletions src/main/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { IpcEvents } from '../ipc-events';
let knownVersions: ElectronVersions;
let localVersions: Array<Version> = [];
let localVersionsPath: string;
let migrated = false;

/**
* Helper to check if this version is from a released major branch.
Expand Down Expand Up @@ -82,7 +81,6 @@ function loadLocalVersions(): void {
const raw = fs.readFileSync(localVersionsPath, 'utf-8');
const data = JSON.parse(raw);
if (data && typeof data === 'object') {
migrated = !!data.migrated;
localVersions = Array.isArray(data.versions)
? data.versions.filter(
(v: any) =>
Expand All @@ -105,7 +103,7 @@ function persistLocalVersions(): void {
try {
fs.writeFileSync(
localVersionsPath,
JSON.stringify({ migrated, versions: localVersions }, null, 2),
JSON.stringify({ versions: localVersions }, null, 2),
);
} catch (err) {
console.warn('Failed to save local versions:', err);
Expand Down Expand Up @@ -182,38 +180,6 @@ export function removeLocalVersion(version: string): Array<Version> {
return localVersions;
}

/**
* One-time migration of local versions from the renderer's localStorage
* to the main process store. Returns true if the migration was accepted,
* false if it was already performed previously.
*/
export function migrateLocalVersions(versions: Array<Version>): boolean {
if (migrated) {
return false;
}

// Validate and merge incoming versions
const validVersions = versions.filter(
(v) =>
v &&
typeof v.version === 'string' &&
v.version.length > 0 &&
typeof v.localPath === 'string' &&
v.localPath.length > 0,
);

for (const ver of validVersions) {
if (!localVersions.find((v) => v.localPath === ver.localPath)) {
localVersions.push(ver);
}
}

migrated = true;
persistLocalVersions();

return true;
}

export async function setupVersions() {
knownVersions = await ElectronVersions.create({
initialVersions: releases,
Expand Down Expand Up @@ -261,12 +227,6 @@ export async function setupVersions() {
event.returnValue = undefined;
},
);
ipcMainManager.on(
IpcEvents.MIGRATE_LOCAL_VERSIONS,
(event, versions: Version[]) => {
event.returnValue = migrateLocalVersions(versions);
},
);
ipcMainManager.on(IpcEvents.GET_OLDEST_SUPPORTED_MAJOR, (event) => {
event.returnValue = getOldestSupportedMajor();
});
Expand Down
3 changes: 0 additions & 3 deletions src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,6 @@ export async function setupFiddleGlobal() {
removeLocalVersion(version: string): Array<Version> {
return ipcRenderer.sendSync(IpcEvents.REMOVE_LOCAL_VERSION, version);
},
migrateLocalVersions(versions: Version[]): boolean {
return ipcRenderer.sendSync(IpcEvents.MIGRATE_LOCAL_VERSIONS, versions);
},
getOldestSupportedMajor() {
return ipcRenderer.sendSync(IpcEvents.GET_OLDEST_SUPPORTED_MAJOR);
},
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { AppState } from './state';
import { activateTheme, getCurrentTheme, getTheme } from './themes';
import { getPackageJson } from './utils/get-package';
import {
discardLocalVersionsFromLocalStorage,
getElectronVersions,
migrateLocalVersionsFromLocalStorage,
} from './versions';
import { PREFERS_DARK_MEDIA_QUERY } from '../constants';
import {
Expand All @@ -36,7 +36,7 @@ export class App {
public readonly electronTypes: ElectronTypes;

constructor() {
migrateLocalVersionsFromLocalStorage();
const legacyLocalVersionsDiscarded = discardLocalVersionsFromLocalStorage();

this.state = new AppState(getElectronVersions());
this.fileManager = new FileManager(this.state);
Expand All @@ -45,6 +45,12 @@ export class App {
this.getEditorValues = this.getEditorValues.bind(this);

this.electronTypes = new ElectronTypes(window.monaco);

if (legacyLocalVersionsDiscarded) {
void this.state.showInfoDialog(
'Fiddle has changed how it stores local Electron versions. Existing local versions have been removed and must be re-added.',
);
}
}

private confirmReplaceUnsaved(): Promise<boolean> {
Expand Down
12 changes: 7 additions & 5 deletions src/renderer/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import {
} from '../interfaces';

/**
* One-time migration: move local versions from localStorage to main process.
* Must be called before getElectronVersions().
* Legacy builds stored local versions in renderer localStorage - discard them.
* Returns true if any legacy local versions were found and discarded, else false.
*/
export function migrateLocalVersionsFromLocalStorage(): void {
export function discardLocalVersionsFromLocalStorage(): boolean {
const raw = window.localStorage.getItem(GlobalSetting.localVersion);
if (!raw) return;
if (!raw) return false;

try {
const versions: Array<Version> = JSON.parse(raw);
if (Array.isArray(versions) && versions.length !== 0) {
window.ElectronFiddle.migrateLocalVersions(versions);
return true;
}
} catch {
// We tried our best, if something is corrupt just remove and move on
} finally {
window.localStorage.removeItem(GlobalSetting.localVersion);
}

return false;
}

/**
Expand Down
64 changes: 1 addition & 63 deletions tests/main/versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
getOldestSupportedMajor,
getReleasedVersions,
isReleasedMajor,
migrateLocalVersions,
removeLocalVersion,
setPendingLocalPath,
setupVersions,
Expand Down Expand Up @@ -102,7 +101,7 @@ describe('versions', () => {
});

describe('local version management', () => {
const emptyState = JSON.stringify({ migrated: false, versions: [] });
const emptyState = JSON.stringify({ versions: [] });

beforeEach(async () => {
// Provide clean persisted state so loadLocalVersions resets properly
Expand Down Expand Up @@ -181,66 +180,9 @@ describe('versions', () => {
});
});

describe('migrateLocalVersions()', () => {
it('accepts migration on first call', () => {
const versions: Version[] = [
{ version: '1.0.0', localPath: '/path/a' },
{ version: '2.0.0', localPath: '/path/b' },
];

const accepted = migrateLocalVersions(versions);
expect(accepted).toBe(true);
expect(getLocalVersions()).toEqual(versions);
expect(fs.writeFileSync).toHaveBeenCalled();
});

it('rejects migration on subsequent calls', () => {
migrateLocalVersions([{ version: '1.0.0', localPath: '/path/a' }]);
vi.mocked(fs.writeFileSync).mockClear();

const accepted = migrateLocalVersions([
{ version: '3.0.0', localPath: '/path/c' },
]);
expect(accepted).toBe(false);
expect(fs.writeFileSync).not.toHaveBeenCalled();
});

it('filters out invalid entries', () => {
const versions = [
{ version: '1.0.0', localPath: '/valid' },
{ version: '2.0.0', localPath: '' },
{ version: null, localPath: '/no-version' },
null,
] as unknown as Version[];

migrateLocalVersions(versions);
expect(getLocalVersions()).toEqual([
{ version: '1.0.0', localPath: '/valid' },
]);
});

it('does not add duplicates by localPath', () => {
setPendingLocalPath('token-dup', '/path/a');
addLocalVersion('token-dup', 'build a');

migrateLocalVersions([
{ version: '1.0.0-dup', localPath: '/path/a' },
{ version: '2.0.0', localPath: '/path/b' },
]);

const locals = getLocalVersions();
expect(locals.filter((v) => v.localPath === '/path/a')).toHaveLength(1);
expect(locals).toContainEqual({
version: '2.0.0',
localPath: '/path/b',
});
});
});

describe('loadLocalVersions()', () => {
it('loads persisted versions on setup', async () => {
const stored = {
migrated: true,
versions: [{ version: '5.0.0', localPath: '/stored' }],
};
vi.mocked(fs.existsSync).mockReturnValue(true);
Expand All @@ -249,10 +191,6 @@ describe('versions', () => {
await setupVersions();

expect(getLocalVersions()).toEqual(stored.versions);
// Since migrated was true, further migration should be rejected
expect(
migrateLocalVersions([{ version: '6.0.0', localPath: '/new' }]),
).toBe(false);
});
});
});
Expand Down
1 change: 0 additions & 1 deletion tests/mocks/electron-fiddle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export class ElectronFiddleMock {
public addLocalVersion = vi.fn();
public cancelPendingLocalVersion = vi.fn();
public removeLocalVersion = vi.fn();
public migrateLocalVersions = vi.fn();
public getNodeTypes = vi.fn();
public getOldestSupportedMajor = vi.fn();
public getReleaseInfo = vi.fn();
Expand Down
38 changes: 38 additions & 0 deletions tests/renderer/versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../../src/interfaces';
import {
addLocalVersion,
discardLocalVersionsFromLocalStorage,
fetchVersions,
getDefaultVersion,
getLocalVersions,
Expand Down Expand Up @@ -125,4 +126,41 @@ describe('versions', () => {
);
});
});

describe('discardLocalVersionsFromLocalStorage()', () => {
it('returns true and removes legacy local versions', () => {
vi.mocked(localStorage.getItem).mockReturnValue(
JSON.stringify([{ version: '1.0.0', localPath: '/legacy/path' }]),
);

const result = discardLocalVersionsFromLocalStorage();

expect(result).toBe(true);
expect(localStorage.removeItem).toHaveBeenCalledWith(
GlobalSetting.localVersion,
);
});

it('returns false for empty legacy versions but still removes key', () => {
vi.mocked(localStorage.getItem).mockReturnValue(JSON.stringify([]));

const result = discardLocalVersionsFromLocalStorage();

expect(result).toBe(false);
expect(localStorage.removeItem).toHaveBeenCalledWith(
GlobalSetting.localVersion,
);
});

it('returns false and removes legacy key even when value is invalid json', () => {
vi.mocked(localStorage.getItem).mockReturnValue('not-json');

const result = discardLocalVersionsFromLocalStorage();

expect(result).toBe(false);
expect(localStorage.removeItem).toHaveBeenCalledWith(
GlobalSetting.localVersion,
);
});
});
});
Loading