diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49ed3972..0bec4d73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,7 +134,9 @@ jobs: draft: true generate_release_notes: true files: | - ${{ matrix.platform == 'win' && 'out/make/**/*.{exe,msix,appx}' || 'out/make/**/*' }} + ${{ matrix.platform == 'win' && 'out/make/**/*.exe + out/make/**/*.msix + out/make/**/*.appx' || 'out/make/**/*' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/features/filesystemPlugin.feature b/features/filesystemPlugin.feature index dd2f1016..206f8e84 100644 --- a/features/filesystemPlugin.feature +++ b/features/filesystemPlugin.feature @@ -83,6 +83,7 @@ Feature: Filesystem Plugin # Modify the tiddler file externally - need to preserve .tid format with metadata When I modify file "{tmpDir}/SubWiki/TestTiddlerTitle.tid" to contain "Content modified in SubWiki symlink" # Wait for watch-fs to detect the change + And I wait for 1 seconds for "watch-fs to detect file change" Then I wait for tiddler "TestTiddlerTitle" to be updated by watch-fs And I wait for 2 seconds # Verify the modified content appears in the wiki diff --git a/package.json b/package.json index ade033bc..6939ba29 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tidgi", "productName": "TidGi", "description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.", - "version": "0.13.0-prerelease7", + "version": "0.13.0-prerelease10", "license": "MPL 2.0", "packageManager": "pnpm@10.18.2", "scripts": { diff --git a/src/services/wiki/plugin/watchFileSystemAdaptor/WatchFileSystemAdaptor.ts b/src/services/wiki/plugin/watchFileSystemAdaptor/WatchFileSystemAdaptor.ts index 32b9a50b..d6db767a 100644 --- a/src/services/wiki/plugin/watchFileSystemAdaptor/WatchFileSystemAdaptor.ts +++ b/src/services/wiki/plugin/watchFileSystemAdaptor/WatchFileSystemAdaptor.ts @@ -63,8 +63,10 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { private inverseFilesIndex: InverseFilesIndex = new InverseFilesIndex(); /** NSFW watcher instance for main wiki */ private watcher: nsfw.NSFW | undefined; - /** Base excluded paths (permanent) */ + /** Base excluded paths (permanent) - absolute paths for main wiki */ private baseExcludedPaths: string[] = []; + /** Excluded path patterns that apply to all wikis (main and sub-wikis) */ + private readonly excludedPathPatterns: string[] = ['.git', 'node_modules', '.DS_Store']; /** * Track pending file deletions to handle git revert/checkout scenarios. * Maps absolute file path to deletion timer. @@ -242,12 +244,11 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { // Initialize inverse index from boot.files this.initializeInverseFilesIndex(); - // Setup base excluded paths (permanent exclusions) + // Setup base excluded paths (permanent exclusions) for main wiki + // Only include paths that are subdirectories of watchPathBase this.baseExcludedPaths = [ path.join(this.watchPathBase, 'subwiki'), - path.join(this.watchPathBase, '.git'), path.join(this.watchPathBase, '$__StoryList'), - path.join(this.watchPathBase, '.DS_Store'), ]; // Setup nsfw watcher @@ -262,7 +263,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { errorCallback: (error) => { this.logger.alert('WatchFileSystemAdaptor NSFW error:', error); }, - // Start with base excluded paths + // Start with base excluded paths - nsfw will filter these at the native level // @ts-expect-error - nsfw types are incorrect, it accepts string[] not just [string] excludedPaths: [...this.baseExcludedPaths], }, @@ -320,6 +321,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { errorCallback: (error) => { this.logger.alert(`WatchFileSystemAdaptor NSFW error for sub-wiki ${subWiki.name}:`, error); }, + // Exclude common patterns for sub-wikis (e.g., .git, node_modules) + // @ts-expect-error - nsfw types are incorrect, it accepts string[] not just [string] + excludedPaths: this.excludedPathPatterns.map(pattern => path.join(subWikiPath, pattern)), }, ); @@ -416,6 +420,23 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { this.pendingDeletions.set(fileAbsolutePath, timer); } + /** + * Check if a path contains any excluded pattern (like .git, node_modules) + * This checks all parts of the path, so it will catch: + * - Direct .git directories: wiki/.git/config + * - Sub-wiki .git directories: wiki/tiddlers/subwiki/.git/index.lock + * - Symlinked .git directories: wiki/tiddlers/link-to-subwiki/.git/config + * @param filePath File or directory path to check + * @returns true if path should be excluded + */ + private shouldExcludeByPattern(filePath: string): boolean { + // Check if any part of the path contains excluded patterns + return this.excludedPathPatterns.some(pattern => { + const pathParts = filePath.split(path.sep); + return pathParts.includes(pattern); + }); + } + /** * Handle NSFW file system change events */ @@ -436,7 +457,28 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { // Compute absolute path const fileAbsolutePath = path.join(directory, fileName); - // Check if this file is in our exclusion list - if so, skip processing + // Early check: skip files in excluded patterns (e.g., .git, node_modules) + if (this.shouldExcludeByPattern(fileAbsolutePath) || this.shouldExcludeByPattern(directory)) { + this.logger.log(`WatchFileSystemAdaptor Skipping file in excluded pattern directory: ${fileAbsolutePath}`); + continue; + } + + // Early check: skip if it's a directory (nsfw sometimes reports directory changes) + // Must check before processing to avoid creating tiddlers for .git files + if (action === nsfw.actions.CREATED || action === nsfw.actions.MODIFIED) { + try { + const stats = fs.statSync(fileAbsolutePath); + if (stats.isDirectory()) { + this.logger.log(`WatchFileSystemAdaptor Skipping directory: ${fileAbsolutePath}`); + continue; + } + } catch { + // File might have been deleted already, skip + continue; + } + } + + // Check if this file is in our dynamic exclusion list (for files being saved/deleted by the app) const subWikiForExclusion = this.inverseFilesIndex.getSubWikiForFile(fileAbsolutePath); const isExcluded = subWikiForExclusion ? this.inverseFilesIndex.isSubWikiFileExcluded(subWikiForExclusion.id, fileAbsolutePath) @@ -462,18 +504,6 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { // Handle different event types if (action === nsfw.actions.CREATED || action === nsfw.actions.MODIFIED) { - // Skip if it's a directory (nsfw sometimes reports directory changes) - try { - const stats = fs.statSync(fileAbsolutePath); - if (stats.isDirectory()) { - this.logger.log(`WatchFileSystemAdaptor Skipping directory: ${fileAbsolutePath}`); - continue; - } - } catch { - // File might have been deleted already, skip - continue; - } - // Cancel any pending deletion for this file (e.g., git revert scenario) this.cancelPendingDeletion(fileAbsolutePath); @@ -595,10 +625,14 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor { const isCreatingNewNonTiddlerFile = changeType === 'add' && !fs.existsSync(metaFileAbsolutePath) && !ignoredExtension.includes(fileExtension.slice(1)); if (isCreatingNewNonTiddlerFile) { const createdTime = $tw.utils.formatDateString(new Date(), '[UTC]YYYY0MM0DD0hh0mm0ss0XXX'); + // Exclude the .meta file before creating it to avoid triggering another watch event + this.excludeFile(metaFileAbsolutePath); fs.writeFileSync( metaFileAbsolutePath, `caption: ${fileNameBase}\ncreated: ${createdTime}\nmodified: ${createdTime}\ntitle: ${fileName}\ntype: ${fileMimeType}\n`, ); + // Schedule re-inclusion after delay + this.scheduleFileInclusion(metaFileAbsolutePath); // After creating .meta, continue to process the file normally // TiddlyWiki will detect the .meta file on next event }