|
2 | 2 | (window as any).Buffer = require('buffer').Buffer; |
3 | 3 |
|
4 | 4 | import { FsaNodeFs } from '../../src/fsa-to-node'; |
5 | | -import type * as fsa from '../../src/fsa/types'; |
6 | | - |
| 5 | +import type { IFileSystemDirectoryHandle } from '../../src/fsa'; |
7 | 6 | import git from 'isomorphic-git'; |
| 7 | +import { clearDirectoryHandle, loadDirectoryHandle, saveDirectoryHandle } from './handle-store'; |
8 | 8 |
|
9 | | -const demo = async (dir: fsa.IFileSystemDirectoryHandle) => { |
10 | | - try { |
11 | | - const fs = ((<any>window).fs = new FsaNodeFs(dir)); |
| 9 | +const REPO_DIR = '/repo'; |
| 10 | +const PERMISSION = { mode: 'readwrite' } as const; |
12 | 11 |
|
13 | | - console.log('Create "/repo" folder'); |
14 | | - await fs.promises.mkdir('/repo'); |
| 12 | +type BrowserDirectoryHandle = FileSystemDirectoryHandle & { |
| 13 | + queryPermission(descriptor: typeof PERMISSION): Promise<PermissionState>; |
| 14 | + requestPermission(descriptor: typeof PERMISSION): Promise<PermissionState>; |
| 15 | +}; |
15 | 16 |
|
16 | | - console.log('Init git repo'); |
17 | | - await git.init({ fs, dir: 'repo' }); |
| 17 | +type DemoWindow = Window & { |
| 18 | + fs?: FsaNodeFs; |
| 19 | + showDirectoryPicker?: (options: { id: string; mode: 'readwrite' }) => Promise<BrowserDirectoryHandle>; |
| 20 | +}; |
18 | 21 |
|
19 | | - console.log('Create README file'); |
20 | | - await fs.promises.writeFile('/repo/README.md', 'Hello World\n'); |
| 22 | +const demoWindow = window as DemoWindow; |
21 | 23 |
|
22 | | - console.log('Stage README file'); |
23 | | - await git.add({ fs, dir: '/repo', filepath: 'README.md' }); |
| 24 | +const isMissing = (error: unknown): boolean => |
| 25 | + !!error && |
| 26 | + typeof error === 'object' && |
| 27 | + ('code' in error ? error.code === 'ENOENT' : 'name' in error && error.name === 'NotFoundError'); |
24 | 28 |
|
25 | | - console.log('Commit README file'); |
26 | | - await git.commit({ |
27 | | - fs, |
28 | | - dir: '/repo', |
29 | | - author: { name: 'Git', email: 'leonid@kingdom.com' }, |
30 | | - message: 'fea: initial commit', |
31 | | - }); |
| 29 | +const hasRepository = async (fs: FsaNodeFs): Promise<boolean> => { |
| 30 | + try { |
| 31 | + return (await fs.promises.stat(`${REPO_DIR}/.git`)).isDirectory(); |
32 | 32 | } catch (error) { |
33 | | - console.log(error); |
34 | | - console.log((<any>error).name); |
| 33 | + if (isMissing(error)) return false; |
| 34 | + throw error; |
35 | 35 | } |
36 | 36 | }; |
37 | 37 |
|
38 | | -const main = async () => { |
39 | | - const button = document.createElement('button'); |
40 | | - button.textContent = 'Select an empty folder'; |
41 | | - document.body.appendChild(button); |
42 | | - button.onclick = async () => { |
43 | | - const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' }); |
44 | | - await demo(dir); |
| 38 | +const createRepository = async (fs: FsaNodeFs): Promise<void> => { |
| 39 | + console.log(`Create "${REPO_DIR}" folder`); |
| 40 | + await fs.promises.mkdir(REPO_DIR, { recursive: true }); |
| 41 | + |
| 42 | + console.log('Init git repo'); |
| 43 | + await git.init({ fs, dir: REPO_DIR }); |
| 44 | + |
| 45 | + console.log('Create README file'); |
| 46 | + await fs.promises.writeFile(`${REPO_DIR}/README.md`, 'Hello World\n'); |
| 47 | + |
| 48 | + console.log('Stage README file'); |
| 49 | + await git.add({ fs, dir: REPO_DIR, filepath: 'README.md' }); |
| 50 | + |
| 51 | + console.log('Commit README file'); |
| 52 | + await git.commit({ |
| 53 | + fs, |
| 54 | + dir: REPO_DIR, |
| 55 | + author: { name: 'Git', email: 'leonid@kingdom.com' }, |
| 56 | + message: 'fea: initial commit', |
| 57 | + }); |
| 58 | +}; |
| 59 | + |
| 60 | +const openDirectory = async (handle: BrowserDirectoryHandle): Promise<'created' | 'reopened'> => { |
| 61 | + const root = handle as unknown as IFileSystemDirectoryHandle; |
| 62 | + const fs = (demoWindow.fs = new FsaNodeFs(root)); |
| 63 | + if (!(await hasRepository(fs))) { |
| 64 | + await createRepository(fs); |
| 65 | + return 'created'; |
| 66 | + } |
| 67 | + const [latestCommit] = await git.log({ fs, dir: REPO_DIR, depth: 1 }); |
| 68 | + console.log(`Reopened "${handle.name}" at commit ${latestCommit.oid}`); |
| 69 | + return 'reopened'; |
| 70 | +}; |
| 71 | + |
| 72 | +const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error)); |
| 73 | + |
| 74 | +const isAbort = (error: unknown): boolean => |
| 75 | + !!error && typeof error === 'object' && 'name' in error && error.name === 'AbortError'; |
| 76 | + |
| 77 | +const main = async (): Promise<void> => { |
| 78 | + const heading = document.createElement('h1'); |
| 79 | + heading.textContent = 'Git in a real folder'; |
| 80 | + const status = document.createElement('p'); |
| 81 | + const selectButton = document.createElement('button'); |
| 82 | + selectButton.textContent = 'Select an empty folder'; |
| 83 | + const reconnectButton = document.createElement('button'); |
| 84 | + reconnectButton.textContent = 'Reconnect saved folder'; |
| 85 | + reconnectButton.hidden = true; |
| 86 | + const forgetButton = document.createElement('button'); |
| 87 | + forgetButton.textContent = 'Forget saved folder'; |
| 88 | + forgetButton.hidden = true; |
| 89 | + document.body.append(heading, status, selectButton, reconnectButton, forgetButton); |
| 90 | + |
| 91 | + let savedHandle: BrowserDirectoryHandle | undefined; |
| 92 | + |
| 93 | + const setBusy = (busy: boolean): void => { |
| 94 | + selectButton.disabled = busy; |
| 95 | + reconnectButton.disabled = busy; |
| 96 | + forgetButton.disabled = busy; |
| 97 | + }; |
| 98 | + |
| 99 | + const showOpened = (handle: BrowserDirectoryHandle, result: 'created' | 'reopened'): void => { |
| 100 | + status.textContent = |
| 101 | + result === 'created' |
| 102 | + ? `Created the demo repository in "${handle.name}".` |
| 103 | + : `Reopened the demo repository in "${handle.name}".`; |
| 104 | + reconnectButton.hidden = true; |
| 105 | + forgetButton.hidden = false; |
| 106 | + }; |
| 107 | + |
| 108 | + const activate = async (handle: BrowserDirectoryHandle): Promise<void> => { |
| 109 | + showOpened(handle, await openDirectory(handle)); |
| 110 | + }; |
| 111 | + |
| 112 | + selectButton.onclick = async () => { |
| 113 | + if (!demoWindow.showDirectoryPicker) return; |
| 114 | + setBusy(true); |
| 115 | + try { |
| 116 | + const handle = await demoWindow.showDirectoryPicker({ id: 'git-fsa-demo', mode: 'readwrite' }); |
| 117 | + await activate(handle); |
| 118 | + await saveDirectoryHandle(handle); |
| 119 | + savedHandle = handle; |
| 120 | + } catch (error) { |
| 121 | + if (!isAbort(error)) status.textContent = `Could not open the folder: ${errorMessage(error)}`; |
| 122 | + } finally { |
| 123 | + setBusy(false); |
| 124 | + } |
| 125 | + }; |
| 126 | + |
| 127 | + reconnectButton.onclick = async () => { |
| 128 | + if (!savedHandle) return; |
| 129 | + setBusy(true); |
| 130 | + try { |
| 131 | + if ((await savedHandle.requestPermission(PERMISSION)) === 'granted') await activate(savedHandle); |
| 132 | + else status.textContent = `Permission was not granted for "${savedHandle.name}".`; |
| 133 | + } catch (error) { |
| 134 | + status.textContent = `Could not reconnect the folder: ${errorMessage(error)}`; |
| 135 | + } finally { |
| 136 | + setBusy(false); |
| 137 | + } |
45 | 138 | }; |
| 139 | + |
| 140 | + forgetButton.onclick = async () => { |
| 141 | + setBusy(true); |
| 142 | + try { |
| 143 | + await clearDirectoryHandle(); |
| 144 | + savedHandle = undefined; |
| 145 | + delete demoWindow.fs; |
| 146 | + status.textContent = 'Forgot the saved folder.'; |
| 147 | + reconnectButton.hidden = true; |
| 148 | + forgetButton.hidden = true; |
| 149 | + } catch (error) { |
| 150 | + status.textContent = `Could not forget the folder: ${errorMessage(error)}`; |
| 151 | + } finally { |
| 152 | + setBusy(false); |
| 153 | + } |
| 154 | + }; |
| 155 | + |
| 156 | + if (!demoWindow.showDirectoryPicker) { |
| 157 | + selectButton.disabled = true; |
| 158 | + status.textContent = 'This browser does not support the File System Access API.'; |
| 159 | + return; |
| 160 | + } |
| 161 | + |
| 162 | + try { |
| 163 | + savedHandle = (await loadDirectoryHandle()) as BrowserDirectoryHandle | undefined; |
| 164 | + if (!savedHandle) { |
| 165 | + status.textContent = 'Select an empty folder to create the demo repository.'; |
| 166 | + return; |
| 167 | + } |
| 168 | + forgetButton.hidden = false; |
| 169 | + if ((await savedHandle.queryPermission(PERMISSION)) === 'granted') { |
| 170 | + setBusy(true); |
| 171 | + try { |
| 172 | + await activate(savedHandle); |
| 173 | + } finally { |
| 174 | + setBusy(false); |
| 175 | + } |
| 176 | + } else { |
| 177 | + status.textContent = `Reconnect "${savedHandle.name}" to continue.`; |
| 178 | + reconnectButton.hidden = false; |
| 179 | + } |
| 180 | + } catch (error) { |
| 181 | + status.textContent = `Could not restore the saved folder: ${errorMessage(error)}`; |
| 182 | + reconnectButton.hidden = !savedHandle; |
| 183 | + } |
46 | 184 | }; |
47 | 185 |
|
48 | 186 | main(); |
0 commit comments