Skip to content

Commit 87eff2a

Browse files
refactor(markdown): remove __spaces__ wrapper from persisted vault layout (#703)
* refactor(markdown): flatten persisted space layout * refactor(markdown): tighten watcher space routing * test(markdown): harden migration and watcher helpers
1 parent 87b79c1 commit 87eff2a

20 files changed

Lines changed: 473 additions & 190 deletions

File tree

src/main/index.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { registerIPC } from './ipc'
99
import { startThemeWatcher, stopThemeWatcher } from './ipc/handlers/theme'
1010
import { mainMenu } from './menu/main'
1111
import { startMarkdownWatcher, stopMarkdownWatcher } from './storage'
12+
import { ensureFlatSpacesLayout } from './storage/providers/markdown/runtime/spaces'
1213
import { store } from './store'
1314
import { checkForUpdates } from './updates'
1415
import { isSqliteFile, log } from './utils'
@@ -106,13 +107,8 @@ else {
106107
store.preferences.get('storagePath') as string,
107108
'markdown-vault',
108109
)
109-
const filePath = path.join(
110-
vaultPath,
111-
'__spaces__',
112-
'notes',
113-
'assets',
114-
fileName,
115-
)
110+
ensureFlatSpacesLayout(vaultPath)
111+
const filePath = path.join(vaultPath, 'notes', 'assets', fileName)
116112

117113
try {
118114
const data = await readFile(filePath)
@@ -148,9 +144,9 @@ else {
148144
const vaultPath
149145
= (store.preferences.get('storage.vaultPath') as string | null)
150146
|| path.join(storagePath, 'markdown-vault')
147+
ensureFlatSpacesLayout(vaultPath)
151148
const statePath = path.join(
152149
vaultPath,
153-
'__spaces__',
154150
'code',
155151
'.masscode',
156152
'state.json',

src/main/ipc/handlers/fs.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ipcMain } from 'electron'
44
import { ensureDirSync, writeFileSync } from 'fs-extra'
55
import { nanoid } from 'nanoid'
66
import slash from 'slash'
7+
import { ensureFlatSpacesLayout } from '../../storage/providers/markdown/runtime/spaces'
78
import { store } from '../../store'
89

910
const ASSETS_DIR = 'assets'
@@ -38,7 +39,8 @@ export function registerFsHandlers() {
3839

3940
return new Promise((resolve, reject) => {
4041
try {
41-
const assetsPath = join(vaultPath, '__spaces__', 'notes', 'assets')
42+
ensureFlatSpacesLayout(vaultPath)
43+
const assetsPath = join(vaultPath, 'notes', 'assets')
4244
const name = `${nanoid()}${ext}`
4345
const dest = join(assetsPath, name)
4446

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
getWatchPathSpaceId,
4+
isCodeWatchPath,
5+
isMathWatchPath,
6+
isNotesWatchPath,
7+
normalizeRelativeWatchPath,
8+
shouldIgnoreWatchPath,
9+
toCodeRelativePath,
10+
} from '../watcherPaths'
11+
12+
describe('watcher routing', () => {
13+
const vaultRoot = '/vault'
14+
15+
it('normalizes relative watch paths under the vault root', () => {
16+
expect(normalizeRelativeWatchPath(vaultRoot, '/vault/code/demo.md')).toBe(
17+
'code/demo.md',
18+
)
19+
expect(normalizeRelativeWatchPath(vaultRoot, 'notes/note.md')).toBe(
20+
'notes/note.md',
21+
)
22+
expect(normalizeRelativeWatchPath(vaultRoot, '/outside/file.md')).toBe(
23+
null,
24+
)
25+
})
26+
27+
it('keeps math state file observable after flat layout migration', () => {
28+
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/math/.state.yaml')).toBe(
29+
false,
30+
)
31+
expect(getWatchPathSpaceId('math/.state.yaml')).toBe('math')
32+
})
33+
34+
it('drops unknown vault-root entries from space routing', () => {
35+
expect(getWatchPathSpaceId('README.md')).toBe(null)
36+
expect(getWatchPathSpaceId('random-dir/file.md')).toBe(null)
37+
})
38+
39+
it('keeps known space roots observable even for dotfiles', () => {
40+
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/code/.gitkeep')).toBe(
41+
false,
42+
)
43+
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/notes/.obsidian')).toBe(
44+
false,
45+
)
46+
})
47+
48+
it('ignores hidden non-space paths', () => {
49+
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/.git/config')).toBe(true)
50+
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/random/.cache/file')).toBe(
51+
true,
52+
)
53+
})
54+
55+
it('recognizes code, notes and math space paths', () => {
56+
expect(isCodeWatchPath('code/demo.md')).toBe(true)
57+
expect(isCodeWatchPath('notes/demo.md')).toBe(false)
58+
expect(isNotesWatchPath('notes/demo.md')).toBe(true)
59+
expect(isMathWatchPath('math/.state.yaml')).toBe(true)
60+
})
61+
62+
it('extracts code-relative paths only from code space entries', () => {
63+
expect(toCodeRelativePath('code')).toBe(null)
64+
expect(toCodeRelativePath('code/folder/demo.md')).toBe('folder/demo.md')
65+
expect(toCodeRelativePath('notes/demo.md')).toBe(null)
66+
})
67+
})

src/main/storage/providers/markdown/notes/runtime/__tests__/constants.test.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ afterEach(() => {
2222
})
2323

2424
describe('getNotesPaths', () => {
25-
it('migrates nested notes space from __spaces__/code to root __spaces__/notes', () => {
25+
it('migrates nested notes space from legacy code root to flat notes root', () => {
2626
const vaultPath = createTempDir()
2727
const legacyNotesRoot = path.join(
2828
vaultPath,
@@ -42,18 +42,16 @@ describe('getNotesPaths', () => {
4242

4343
const notesPaths = getNotesPaths(vaultPath)
4444

45-
expect(notesPaths.notesRoot).toBe(
46-
path.join(vaultPath, '__spaces__', 'notes'),
47-
)
45+
expect(notesPaths.notesRoot).toBe(path.join(vaultPath, 'notes'))
4846
expect(
4947
fs.pathExistsSync(path.join(notesPaths.notesRoot, 'Folder', 'note.md')),
5048
).toBe(true)
5149
expect(fs.pathExistsSync(legacyNotesRoot)).toBe(false)
5250
})
5351

54-
it('merges nested notes space into existing root notes space', () => {
52+
it('merges nested notes space into existing flat notes space', () => {
5553
const vaultPath = createTempDir()
56-
const notesRoot = path.join(vaultPath, '__spaces__', 'notes')
54+
const notesRoot = path.join(vaultPath, 'notes')
5755
fs.ensureDirSync(path.join(notesRoot, '.masscode'))
5856
fs.writeFileSync(
5957
path.join(notesRoot, '.masscode', 'state.json'),
@@ -62,13 +60,7 @@ describe('getNotesPaths', () => {
6260
)
6361
fs.writeFileSync(path.join(notesRoot, 'root-note.md'), '# Root')
6462

65-
const legacyNotesRoot = path.join(
66-
vaultPath,
67-
'__spaces__',
68-
'code',
69-
'__spaces__',
70-
'notes',
71-
)
63+
const legacyNotesRoot = path.join(vaultPath, 'code', '__spaces__', 'notes')
7264
fs.ensureDirSync(legacyNotesRoot)
7365
fs.writeFileSync(path.join(legacyNotesRoot, 'legacy-note.md'), '# Legacy')
7466

src/main/storage/providers/markdown/notes/runtime/constants.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ import {
66
INBOX_DIR_NAME,
77
META_DIR_NAME,
88
META_FILE_NAME,
9-
SPACES_DIR_NAME,
9+
NOTES_SPACE_ID,
1010
STATE_FILE_NAME,
1111
TRASH_DIR_NAME,
1212
} from '../../runtime/constants'
13+
import { getSpaceDirPath } from '../../runtime/spaces'
1314

1415
export { INBOX_DIR_NAME, META_DIR_NAME, META_FILE_NAME, TRASH_DIR_NAME }
1516

16-
export const NOTES_SPACE_ID = 'notes'
17-
1817
export const NOTES_INBOX_RELATIVE_PATH = `${META_DIR_NAME}/${INBOX_DIR_NAME}`
1918
export const NOTES_TRASH_RELATIVE_PATH = `${META_DIR_NAME}/${TRASH_DIR_NAME}`
2019

@@ -28,13 +27,7 @@ export const notesRuntimeRef: { cache: NotesRuntimeCache | null } = {
2827
}
2928

3029
function getLegacyNestedNotesRoot(vaultPath: string): string {
31-
return path.join(
32-
vaultPath,
33-
SPACES_DIR_NAME,
34-
CODE_SPACE_ID,
35-
SPACES_DIR_NAME,
36-
NOTES_SPACE_ID,
37-
)
30+
return path.join(vaultPath, CODE_SPACE_ID, '__spaces__', NOTES_SPACE_ID)
3831
}
3932

4033
function hasNotesState(notesRoot: string): boolean {
@@ -66,7 +59,7 @@ function migrateNestedNotesSpace(vaultPath: string, notesRoot: string): void {
6659
}
6760

6861
export function getNotesPaths(vaultPath: string) {
69-
const notesRoot = path.join(vaultPath, SPACES_DIR_NAME, NOTES_SPACE_ID)
62+
const notesRoot = getSpaceDirPath(vaultPath, NOTES_SPACE_ID)
7063
migrateNestedNotesSpace(vaultPath, notesRoot)
7164
const metaDirPath = path.join(notesRoot, META_DIR_NAME)
7265

src/main/storage/providers/markdown/notes/storages/__tests__/folders.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('folders storage validations', () => {
7878
tempVaultPath = fs.mkdtempSync(path.join(os.tmpdir(), 'folders-storage-'))
7979
resetNotesRuntimeCache()
8080

81-
const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
81+
const notesRoot = path.join(tempVaultPath, 'notes')
8282
const metaDirPath = path.join(notesRoot, '.masscode')
8383

8484
ensureNotesStateFile({
@@ -124,7 +124,7 @@ describe('folders storage validations', () => {
124124
const { id } = storage.createFolder({ name: 'Source' })
125125
storage.getFolders()
126126

127-
const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
127+
const notesRoot = path.join(tempVaultPath, 'notes')
128128
fs.ensureDirSync(path.join(notesRoot, 'Target'))
129129

130130
expect(() => storage.updateFolder(id, { name: 'Target' })).toThrow(

src/main/storage/providers/markdown/notes/storages/__tests__/notes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('notes storage validations', () => {
7878
tempVaultPath = fs.mkdtempSync(path.join(os.tmpdir(), 'notes-storage-'))
7979
resetNotesRuntimeCache()
8080

81-
const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
81+
const notesRoot = path.join(tempVaultPath, 'notes')
8282
const metaDirPath = path.join(notesRoot, '.masscode')
8383

8484
ensureNotesStateFile({

src/main/storage/providers/markdown/runtime/__tests__/paths.test.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ afterEach(() => {
8080
})
8181

8282
describe('getPaths', () => {
83-
it('migrates legacy code vault root into __spaces__/code', () => {
83+
it('migrates legacy code vault root into code', () => {
8484
const vaultPath = createTempDir()
8585

8686
fs.ensureDirSync(path.join(vaultPath, '.masscode'))
@@ -114,7 +114,7 @@ describe('getPaths', () => {
114114

115115
const paths = getPaths(vaultPath)
116116

117-
expect(paths.vaultPath).toBe(path.join(vaultPath, '__spaces__', 'code'))
117+
expect(paths.vaultPath).toBe(path.join(vaultPath, 'code'))
118118
expect(
119119
fs.pathExistsSync(path.join(paths.vaultPath, '.masscode', 'state.json')),
120120
).toBe(true)
@@ -128,7 +128,7 @@ describe('getPaths', () => {
128128

129129
it('keeps existing initialized code space without forced root migration', () => {
130130
const vaultPath = createTempDir()
131-
const codeRoot = path.join(vaultPath, '__spaces__', 'code')
131+
const codeRoot = path.join(vaultPath, 'code')
132132
fs.ensureDirSync(path.join(codeRoot, '.masscode'))
133133
fs.writeJSONSync(path.join(codeRoot, '.masscode', 'state.json'), {
134134
counters: {
@@ -172,7 +172,7 @@ describe('getPaths', () => {
172172

173173
it('migrates when code space exists but is not initialized', () => {
174174
const vaultPath = createTempDir()
175-
const codeRoot = path.join(vaultPath, '__spaces__', 'code')
175+
const codeRoot = path.join(vaultPath, 'code')
176176
fs.ensureDirSync(codeRoot)
177177

178178
fs.ensureDirSync(path.join(vaultPath, '.masscode'))
@@ -203,4 +203,91 @@ describe('getPaths', () => {
203203
)
204204
expect(fs.pathExistsSync(path.join(vaultPath, 'Legacy'))).toBe(false)
205205
})
206+
207+
it('flattens __spaces__ wrapper for all known spaces before resolving code path', () => {
208+
const vaultPath = createTempDir()
209+
const legacyCodeRoot = path.join(vaultPath, '__spaces__', 'code')
210+
const legacyNotesRoot = path.join(vaultPath, '__spaces__', 'notes')
211+
const legacyMathRoot = path.join(vaultPath, '__spaces__', 'math')
212+
213+
fs.ensureDirSync(path.join(legacyCodeRoot, '.masscode'))
214+
fs.writeJSONSync(path.join(legacyCodeRoot, '.masscode', 'state.json'), {
215+
counters: {
216+
contentId: 0,
217+
folderId: 0,
218+
snippetId: 0,
219+
tagId: 0,
220+
},
221+
folderUi: {},
222+
folders: [],
223+
snippets: [],
224+
tags: [],
225+
version: 2,
226+
})
227+
fs.writeFileSync(path.join(legacyCodeRoot, 'snippet.md'), '# Code')
228+
229+
fs.ensureDirSync(legacyNotesRoot)
230+
fs.writeFileSync(path.join(legacyNotesRoot, 'note.md'), '# Note')
231+
232+
fs.ensureDirSync(legacyMathRoot)
233+
fs.writeFileSync(path.join(legacyMathRoot, '.state.yaml'), 'sheets: []')
234+
235+
const paths = getPaths(vaultPath)
236+
237+
expect(paths.vaultPath).toBe(path.join(vaultPath, 'code'))
238+
expect(fs.pathExistsSync(path.join(vaultPath, 'code', 'snippet.md'))).toBe(
239+
true,
240+
)
241+
expect(fs.pathExistsSync(path.join(vaultPath, 'notes', 'note.md'))).toBe(
242+
true,
243+
)
244+
expect(fs.pathExistsSync(path.join(vaultPath, 'math', '.state.yaml'))).toBe(
245+
true,
246+
)
247+
expect(fs.pathExistsSync(path.join(vaultPath, '__spaces__'))).toBe(false)
248+
})
249+
250+
it('merges partially migrated __spaces__ content into existing flat roots', () => {
251+
const vaultPath = createTempDir()
252+
const codeRoot = path.join(vaultPath, 'code')
253+
const legacyCodeRoot = path.join(vaultPath, '__spaces__', 'code')
254+
const notesRoot = path.join(vaultPath, 'notes')
255+
const legacyNotesRoot = path.join(vaultPath, '__spaces__', 'notes')
256+
257+
fs.ensureDirSync(path.join(codeRoot, '.masscode'))
258+
fs.writeJSONSync(path.join(codeRoot, '.masscode', 'state.json'), {
259+
counters: {
260+
contentId: 0,
261+
folderId: 0,
262+
snippetId: 0,
263+
tagId: 0,
264+
},
265+
folderUi: {},
266+
folders: [],
267+
snippets: [],
268+
tags: [],
269+
version: 2,
270+
})
271+
fs.writeFileSync(path.join(codeRoot, 'root.md'), '# Root')
272+
273+
fs.ensureDirSync(legacyCodeRoot)
274+
fs.writeFileSync(path.join(legacyCodeRoot, 'legacy.md'), '# Legacy')
275+
276+
fs.ensureDirSync(notesRoot)
277+
fs.writeFileSync(path.join(notesRoot, 'existing.md'), '# Existing')
278+
279+
fs.ensureDirSync(legacyNotesRoot)
280+
fs.writeFileSync(path.join(legacyNotesRoot, 'migrated.md'), '# Migrated')
281+
282+
const paths = getPaths(vaultPath)
283+
284+
expect(paths.vaultPath).toBe(codeRoot)
285+
expect(fs.pathExistsSync(path.join(codeRoot, 'root.md'))).toBe(true)
286+
expect(fs.pathExistsSync(path.join(codeRoot, 'legacy.md'))).toBe(true)
287+
expect(fs.pathExistsSync(path.join(notesRoot, 'existing.md'))).toBe(true)
288+
expect(fs.pathExistsSync(path.join(notesRoot, 'migrated.md'))).toBe(true)
289+
expect(fs.pathExistsSync(legacyCodeRoot)).toBe(false)
290+
expect(fs.pathExistsSync(legacyNotesRoot)).toBe(false)
291+
expect(fs.pathExistsSync(path.join(vaultPath, '__spaces__'))).toBe(false)
292+
})
206293
})

0 commit comments

Comments
 (0)