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: 4 additions & 8 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { registerIPC } from './ipc'
import { startThemeWatcher, stopThemeWatcher } from './ipc/handlers/theme'
import { mainMenu } from './menu/main'
import { startMarkdownWatcher, stopMarkdownWatcher } from './storage'
import { ensureFlatSpacesLayout } from './storage/providers/markdown/runtime/spaces'
import { store } from './store'
import { checkForUpdates } from './updates'
import { isSqliteFile, log } from './utils'
Expand Down Expand Up @@ -106,13 +107,8 @@ else {
store.preferences.get('storagePath') as string,
'markdown-vault',
)
const filePath = path.join(
vaultPath,
'__spaces__',
'notes',
'assets',
fileName,
)
ensureFlatSpacesLayout(vaultPath)
const filePath = path.join(vaultPath, 'notes', 'assets', fileName)

try {
const data = await readFile(filePath)
Expand Down Expand Up @@ -148,9 +144,9 @@ else {
const vaultPath
= (store.preferences.get('storage.vaultPath') as string | null)
|| path.join(storagePath, 'markdown-vault')
ensureFlatSpacesLayout(vaultPath)
const statePath = path.join(
vaultPath,
'__spaces__',
'code',
'.masscode',
'state.json',
Expand Down
4 changes: 3 additions & 1 deletion src/main/ipc/handlers/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ipcMain } from 'electron'
import { ensureDirSync, writeFileSync } from 'fs-extra'
import { nanoid } from 'nanoid'
import slash from 'slash'
import { ensureFlatSpacesLayout } from '../../storage/providers/markdown/runtime/spaces'
import { store } from '../../store'

const ASSETS_DIR = 'assets'
Expand Down Expand Up @@ -38,7 +39,8 @@ export function registerFsHandlers() {

return new Promise((resolve, reject) => {
try {
const assetsPath = join(vaultPath, '__spaces__', 'notes', 'assets')
ensureFlatSpacesLayout(vaultPath)
const assetsPath = join(vaultPath, 'notes', 'assets')
const name = `${nanoid()}${ext}`
const dest = join(assetsPath, name)

Expand Down
67 changes: 67 additions & 0 deletions src/main/storage/providers/markdown/__tests__/watcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest'
import {
getWatchPathSpaceId,
isCodeWatchPath,
isMathWatchPath,
isNotesWatchPath,
normalizeRelativeWatchPath,
shouldIgnoreWatchPath,
toCodeRelativePath,
} from '../watcherPaths'

describe('watcher routing', () => {
const vaultRoot = '/vault'

it('normalizes relative watch paths under the vault root', () => {
expect(normalizeRelativeWatchPath(vaultRoot, '/vault/code/demo.md')).toBe(
'code/demo.md',
)
expect(normalizeRelativeWatchPath(vaultRoot, 'notes/note.md')).toBe(
'notes/note.md',
)
expect(normalizeRelativeWatchPath(vaultRoot, '/outside/file.md')).toBe(
null,
)
})

it('keeps math state file observable after flat layout migration', () => {
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/math/.state.yaml')).toBe(
false,
)
expect(getWatchPathSpaceId('math/.state.yaml')).toBe('math')
})

it('drops unknown vault-root entries from space routing', () => {
expect(getWatchPathSpaceId('README.md')).toBe(null)
expect(getWatchPathSpaceId('random-dir/file.md')).toBe(null)
})

it('keeps known space roots observable even for dotfiles', () => {
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/code/.gitkeep')).toBe(
false,
)
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/notes/.obsidian')).toBe(
false,
)
})

it('ignores hidden non-space paths', () => {
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/.git/config')).toBe(true)
expect(shouldIgnoreWatchPath(vaultRoot, '/vault/random/.cache/file')).toBe(
true,
)
})

it('recognizes code, notes and math space paths', () => {
expect(isCodeWatchPath('code/demo.md')).toBe(true)
expect(isCodeWatchPath('notes/demo.md')).toBe(false)
expect(isNotesWatchPath('notes/demo.md')).toBe(true)
expect(isMathWatchPath('math/.state.yaml')).toBe(true)
})

it('extracts code-relative paths only from code space entries', () => {
expect(toCodeRelativePath('code')).toBe(null)
expect(toCodeRelativePath('code/folder/demo.md')).toBe('folder/demo.md')
expect(toCodeRelativePath('notes/demo.md')).toBe(null)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ afterEach(() => {
})

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

const notesPaths = getNotesPaths(vaultPath)

expect(notesPaths.notesRoot).toBe(
path.join(vaultPath, '__spaces__', 'notes'),
)
expect(notesPaths.notesRoot).toBe(path.join(vaultPath, 'notes'))
expect(
fs.pathExistsSync(path.join(notesPaths.notesRoot, 'Folder', 'note.md')),
).toBe(true)
expect(fs.pathExistsSync(legacyNotesRoot)).toBe(false)
})

it('merges nested notes space into existing root notes space', () => {
it('merges nested notes space into existing flat notes space', () => {
const vaultPath = createTempDir()
const notesRoot = path.join(vaultPath, '__spaces__', 'notes')
const notesRoot = path.join(vaultPath, 'notes')
fs.ensureDirSync(path.join(notesRoot, '.masscode'))
fs.writeFileSync(
path.join(notesRoot, '.masscode', 'state.json'),
Expand All @@ -62,13 +60,7 @@ describe('getNotesPaths', () => {
)
fs.writeFileSync(path.join(notesRoot, 'root-note.md'), '# Root')

const legacyNotesRoot = path.join(
vaultPath,
'__spaces__',
'code',
'__spaces__',
'notes',
)
const legacyNotesRoot = path.join(vaultPath, 'code', '__spaces__', 'notes')
fs.ensureDirSync(legacyNotesRoot)
fs.writeFileSync(path.join(legacyNotesRoot, 'legacy-note.md'), '# Legacy')

Expand Down
15 changes: 4 additions & 11 deletions src/main/storage/providers/markdown/notes/runtime/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import {
INBOX_DIR_NAME,
META_DIR_NAME,
META_FILE_NAME,
SPACES_DIR_NAME,
NOTES_SPACE_ID,
STATE_FILE_NAME,
TRASH_DIR_NAME,
} from '../../runtime/constants'
import { getSpaceDirPath } from '../../runtime/spaces'

export { INBOX_DIR_NAME, META_DIR_NAME, META_FILE_NAME, TRASH_DIR_NAME }

export const NOTES_SPACE_ID = 'notes'

export const NOTES_INBOX_RELATIVE_PATH = `${META_DIR_NAME}/${INBOX_DIR_NAME}`
export const NOTES_TRASH_RELATIVE_PATH = `${META_DIR_NAME}/${TRASH_DIR_NAME}`

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

function getLegacyNestedNotesRoot(vaultPath: string): string {
return path.join(
vaultPath,
SPACES_DIR_NAME,
CODE_SPACE_ID,
SPACES_DIR_NAME,
NOTES_SPACE_ID,
)
return path.join(vaultPath, CODE_SPACE_ID, '__spaces__', NOTES_SPACE_ID)
}

function hasNotesState(notesRoot: string): boolean {
Expand Down Expand Up @@ -66,7 +59,7 @@ function migrateNestedNotesSpace(vaultPath: string, notesRoot: string): void {
}

export function getNotesPaths(vaultPath: string) {
const notesRoot = path.join(vaultPath, SPACES_DIR_NAME, NOTES_SPACE_ID)
const notesRoot = getSpaceDirPath(vaultPath, NOTES_SPACE_ID)
migrateNestedNotesSpace(vaultPath, notesRoot)
const metaDirPath = path.join(notesRoot, META_DIR_NAME)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('folders storage validations', () => {
tempVaultPath = fs.mkdtempSync(path.join(os.tmpdir(), 'folders-storage-'))
resetNotesRuntimeCache()

const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
const notesRoot = path.join(tempVaultPath, 'notes')
const metaDirPath = path.join(notesRoot, '.masscode')

ensureNotesStateFile({
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('folders storage validations', () => {
const { id } = storage.createFolder({ name: 'Source' })
storage.getFolders()

const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
const notesRoot = path.join(tempVaultPath, 'notes')
fs.ensureDirSync(path.join(notesRoot, 'Target'))

expect(() => storage.updateFolder(id, { name: 'Target' })).toThrow(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('notes storage validations', () => {
tempVaultPath = fs.mkdtempSync(path.join(os.tmpdir(), 'notes-storage-'))
resetNotesRuntimeCache()

const notesRoot = path.join(tempVaultPath, '__spaces__', 'notes')
const notesRoot = path.join(tempVaultPath, 'notes')
const metaDirPath = path.join(notesRoot, '.masscode')

ensureNotesStateFile({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ afterEach(() => {
})

describe('getPaths', () => {
it('migrates legacy code vault root into __spaces__/code', () => {
it('migrates legacy code vault root into code', () => {
const vaultPath = createTempDir()

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

const paths = getPaths(vaultPath)

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

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

it('migrates when code space exists but is not initialized', () => {
const vaultPath = createTempDir()
const codeRoot = path.join(vaultPath, '__spaces__', 'code')
const codeRoot = path.join(vaultPath, 'code')
fs.ensureDirSync(codeRoot)

fs.ensureDirSync(path.join(vaultPath, '.masscode'))
Expand Down Expand Up @@ -203,4 +203,91 @@ describe('getPaths', () => {
)
expect(fs.pathExistsSync(path.join(vaultPath, 'Legacy'))).toBe(false)
})

it('flattens __spaces__ wrapper for all known spaces before resolving code path', () => {
const vaultPath = createTempDir()
const legacyCodeRoot = path.join(vaultPath, '__spaces__', 'code')
const legacyNotesRoot = path.join(vaultPath, '__spaces__', 'notes')
const legacyMathRoot = path.join(vaultPath, '__spaces__', 'math')

fs.ensureDirSync(path.join(legacyCodeRoot, '.masscode'))
fs.writeJSONSync(path.join(legacyCodeRoot, '.masscode', 'state.json'), {
counters: {
contentId: 0,
folderId: 0,
snippetId: 0,
tagId: 0,
},
folderUi: {},
folders: [],
snippets: [],
tags: [],
version: 2,
})
fs.writeFileSync(path.join(legacyCodeRoot, 'snippet.md'), '# Code')

fs.ensureDirSync(legacyNotesRoot)
fs.writeFileSync(path.join(legacyNotesRoot, 'note.md'), '# Note')

fs.ensureDirSync(legacyMathRoot)
fs.writeFileSync(path.join(legacyMathRoot, '.state.yaml'), 'sheets: []')

const paths = getPaths(vaultPath)

expect(paths.vaultPath).toBe(path.join(vaultPath, 'code'))
expect(fs.pathExistsSync(path.join(vaultPath, 'code', 'snippet.md'))).toBe(
true,
)
expect(fs.pathExistsSync(path.join(vaultPath, 'notes', 'note.md'))).toBe(
true,
)
expect(fs.pathExistsSync(path.join(vaultPath, 'math', '.state.yaml'))).toBe(
true,
)
expect(fs.pathExistsSync(path.join(vaultPath, '__spaces__'))).toBe(false)
})

it('merges partially migrated __spaces__ content into existing flat roots', () => {
const vaultPath = createTempDir()
const codeRoot = path.join(vaultPath, 'code')
const legacyCodeRoot = path.join(vaultPath, '__spaces__', 'code')
const notesRoot = path.join(vaultPath, 'notes')
const legacyNotesRoot = path.join(vaultPath, '__spaces__', 'notes')

fs.ensureDirSync(path.join(codeRoot, '.masscode'))
fs.writeJSONSync(path.join(codeRoot, '.masscode', 'state.json'), {
counters: {
contentId: 0,
folderId: 0,
snippetId: 0,
tagId: 0,
},
folderUi: {},
folders: [],
snippets: [],
tags: [],
version: 2,
})
fs.writeFileSync(path.join(codeRoot, 'root.md'), '# Root')

fs.ensureDirSync(legacyCodeRoot)
fs.writeFileSync(path.join(legacyCodeRoot, 'legacy.md'), '# Legacy')

fs.ensureDirSync(notesRoot)
fs.writeFileSync(path.join(notesRoot, 'existing.md'), '# Existing')

fs.ensureDirSync(legacyNotesRoot)
fs.writeFileSync(path.join(legacyNotesRoot, 'migrated.md'), '# Migrated')

const paths = getPaths(vaultPath)

expect(paths.vaultPath).toBe(codeRoot)
expect(fs.pathExistsSync(path.join(codeRoot, 'root.md'))).toBe(true)
expect(fs.pathExistsSync(path.join(codeRoot, 'legacy.md'))).toBe(true)
expect(fs.pathExistsSync(path.join(notesRoot, 'existing.md'))).toBe(true)
expect(fs.pathExistsSync(path.join(notesRoot, 'migrated.md'))).toBe(true)
expect(fs.pathExistsSync(legacyCodeRoot)).toBe(false)
expect(fs.pathExistsSync(legacyNotesRoot)).toBe(false)
expect(fs.pathExistsSync(path.join(vaultPath, '__spaces__'))).toBe(false)
})
})
Loading