dsh-folder-path 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,10 +128,13 @@ dsh plugin --profile <profile> remove dsh-folder-path
128
128
  ## 开发
129
129
 
130
130
  ```sh
131
- npm run build # client/index.js -> lib/client.js(把浏览器半边包成 __ModuleLoader__ bundle)
132
- npm test # 语法检查 + 宿主路由 / 浏览器半契约 / 清单契约测试(node --test,无需 DSH 服务器)
131
+ npm run build # tsc: src/index.ts -> lib/index.js,client/index.ts -> .client-build/index.js;再包成 lib/client.js
132
+ npm test # 先 build,再跑宿主路由 / 浏览器半契约 / 清单契约测试(node --test,无需 DSH 服务器)
133
133
  ```
134
134
 
135
+ 宿主半边源码在 `src/index.ts`(TypeScript,`lib/index.js` 与 `lib/types/` 是 tsc 产物,随仓库提交);
136
+ 浏览器半边是 `client/index.ts`(`.client-build/index.js` 是 tsc 产物),由构建脚本包成 `lib/client.js`。
137
+
135
138
  浏览器侧调试钩子(devtools 控制台):
136
139
 
137
140
  ```js
package/README_EN.md CHANGED
@@ -145,8 +145,8 @@ or this package's own `cordis.patch.yml`, which `dsh plugin add` wires in):
145
145
  ## Development
146
146
 
147
147
  ```sh
148
- npm run build # client/index.js -> lib/client.js (wraps the browser half as a __ModuleLoader__ bundle)
149
- npm test # syntax checks + host route / browser-half contract / manifest contract tests
148
+ npm run build # client/index.ts -> .client-build/index.js -> lib/client.js (wraps the browser half as a __ModuleLoader__ bundle)
149
+ npm test # builds both halves, then host route / browser-half contract / manifest contract tests
150
150
  # (node --test, no DSH server needed)
151
151
  ```
152
152
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ambient declarations for the browser half.
3
+ *
4
+ * `client/index.ts` is a classic script (it has no imports or exports), so the
5
+ * bundle wrapper in `scripts/build-client.mts` can embed it inside a
6
+ * `window.__ModuleLoader__.load({ id, factory })` factory unchanged. The DOM
7
+ * types come from the `DOM` lib in `tsconfig.client.json`; only the two
8
+ * page globals this plugin defines or consumes are declared here.
9
+ */
10
+
11
+ /** The troubleshooting hook the browser half installs on `window`. */
12
+ interface FolderPathDebugHook {
13
+ /** The host route the plugin calls. */
14
+ route: string
15
+ /** Ask the host for the full paths behind these folder names. */
16
+ probe(names: string[]): Promise<{ paths: string[]; reason: string | null }>
17
+ /** Insert text through DSH's own paste path. */
18
+ insert(root: HTMLElement, text: string): void
19
+ /** Split a paste payload into directories and files. */
20
+ classify(dataTransfer: DataTransfer): { directories: string[]; files: File[] }
21
+ }
22
+
23
+ interface Window {
24
+ /** The DSH client module system; the bundle wrapper registers through it. */
25
+ __ModuleLoader__: {
26
+ load(registration: { id: string; factory: (require: (id: string) => unknown) => unknown }): void
27
+ }
28
+ /** Installed by `apply` so the devtools console can drive the plugin. */
29
+ __dshFolderPath?: FolderPathDebugHook
30
+ }
@@ -24,7 +24,7 @@
24
24
  * returns before `preventDefault()` unless a directory is present.
25
25
  */
26
26
 
27
- /** The host route; hand-mirrored from `lib/index.js` (ROUTE_PATH). */
27
+ /** The host route; hand-mirrored from `src/index.ts` (ROUTE_PATH). */
28
28
  var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths'
29
29
 
30
30
  /** Marks the synthetic events this plugin dispatches, so they are not re-read. */
@@ -36,6 +36,23 @@ var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]'
36
36
  /** How long a failure notice stays on screen. */
37
37
  var TOAST_MS = 5000
38
38
 
39
+ /** Directories and files a paste carried, split apart by `classifyPayload`. */
40
+ interface ClassifiedPayload {
41
+ directories: string[]
42
+ files: File[]
43
+ }
44
+
45
+ /** The host half's answer to a clipboard probe. */
46
+ interface HostAnswer {
47
+ paths: string[]
48
+ reason: string | null
49
+ }
50
+
51
+ /** The browser Cordis context surface this plugin uses. */
52
+ interface BrowserContext {
53
+ effect(factory: () => () => void, label: string): void
54
+ }
55
+
39
56
  /**
40
57
  * Split a paste payload into directories and files.
41
58
  *
@@ -47,16 +64,16 @@ var TOAST_MS = 5000
47
64
  * @param dataTransfer - the event's clipboard or drag data.
48
65
  * @returns directory basenames and the non-directory files.
49
66
  */
50
- function classifyPayload(dataTransfer) {
51
- var directories = []
52
- var files = []
53
- var items = dataTransfer.items ? Array.prototype.slice.call(dataTransfer.items) : []
67
+ function classifyPayload(dataTransfer: DataTransfer): ClassifiedPayload {
68
+ var directories: string[] = []
69
+ var files: File[] = []
70
+ var items: DataTransferItem[] = dataTransfer.items ? Array.prototype.slice.call(dataTransfer.items) : []
54
71
 
55
72
  for (var index = 0; index < items.length; index += 1) {
56
73
  var item = items[index]
57
74
  if (item.kind !== 'file') continue
58
75
 
59
- var entry = null
76
+ var entry: FileSystemEntry | null = null
60
77
  try {
61
78
  entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null
62
79
  } catch (error) {
@@ -72,7 +89,7 @@ function classifyPayload(dataTransfer) {
72
89
  if (entry.name) directories.push(entry.name)
73
90
  continue
74
91
  }
75
- var entryFile = null
92
+ var entryFile: File | null = null
76
93
  try {
77
94
  entryFile = item.getAsFile()
78
95
  } catch (error) {
@@ -82,7 +99,7 @@ function classifyPayload(dataTransfer) {
82
99
  continue
83
100
  }
84
101
 
85
- var file = null
102
+ var file: File | null = null
86
103
  try {
87
104
  file = item.getAsFile()
88
105
  } catch (error) {
@@ -111,13 +128,13 @@ function classifyPayload(dataTransfer) {
111
128
  * @param target - the event target.
112
129
  * @returns the editable root element, or null.
113
130
  */
114
- function editableRootOf(target) {
115
- var candidates = [target, document.activeElement]
131
+ function editableRootOf(target: EventTarget | null): HTMLElement | null {
132
+ var candidates: Array<Element | null> = [target as Element | null, document.activeElement]
116
133
  for (var index = 0; index < candidates.length; index += 1) {
117
134
  var candidate = candidates[index]
118
135
  if (candidate && typeof candidate.closest === 'function') {
119
136
  var root = candidate.closest(EDITABLE_SELECTOR)
120
- if (root !== null) return root
137
+ if (root !== null) return root as HTMLElement
121
138
  }
122
139
  }
123
140
  return null
@@ -128,7 +145,7 @@ function editableRootOf(target) {
128
145
  * @param names - directory basenames the browser saw.
129
146
  * @returns the resolved paths (empty when the host could not answer).
130
147
  */
131
- async function requestPaths(names) {
148
+ async function requestPaths(names: string[]): Promise<HostAnswer> {
132
149
  var response = await fetch(ROUTE_PATH, {
133
150
  method: 'POST',
134
151
  headers: { 'content-type': 'application/json' },
@@ -153,13 +170,13 @@ async function requestPaths(names) {
153
170
  * @param root - the composer's editable root.
154
171
  * @param text - the text to insert.
155
172
  */
156
- function insertText(root, text) {
173
+ function insertText(root: HTMLElement, text: string): void {
157
174
  root.focus({ preventScroll: true })
158
175
  try {
159
176
  var dataTransfer = new DataTransfer()
160
177
  dataTransfer.setData('text/plain', text)
161
178
  var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
162
- event[SYNTHETIC] = true
179
+ ;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
163
180
  root.dispatchEvent(event)
164
181
  if (event.defaultPrevented) return
165
182
  } catch (error) {
@@ -178,13 +195,13 @@ function insertText(root, text) {
178
195
  * @param root - the composer's editable root.
179
196
  * @param files - the files taken out of the original paste.
180
197
  */
181
- function forwardFiles(root, files) {
198
+ function forwardFiles(root: HTMLElement, files: File[]): void {
182
199
  if (files.length === 0) return
183
200
  try {
184
201
  var dataTransfer = new DataTransfer()
185
202
  for (var index = 0; index < files.length; index += 1) dataTransfer.items.add(files[index])
186
203
  var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
187
- event[SYNTHETIC] = true
204
+ ;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
188
205
  root.dispatchEvent(event)
189
206
  } catch (error) {
190
207
  /* the paths are the point; a failed forward must not undo them */
@@ -196,7 +213,7 @@ function forwardFiles(root, files) {
196
213
  * @param reason - the host's reason code.
197
214
  * @returns the message to show.
198
215
  */
199
- function failureText(reason) {
216
+ function failureText(reason: string | null): string {
200
217
  if (reason === 'unsupported-platform') return '当前宿主系统不支持读取剪贴板里的文件夹路径(仅支持 Windows / macOS)'
201
218
  if (reason === 'remote-not-supported') return '当前页面不在运行 dsh web 的这台机器上,读不到这台机器的剪贴板'
202
219
  if (reason === 'clipboard-error') return '读取剪贴板失败,没能解析文件夹的完整路径'
@@ -208,7 +225,7 @@ function failureText(reason) {
208
225
  * Show a short-lived notice above the composer.
209
226
  * @param message - the text to show.
210
227
  */
211
- function toast(message) {
228
+ function toast(message: string): void {
212
229
  try {
213
230
  var element = document.createElement('div')
214
231
  element.textContent = message
@@ -231,15 +248,15 @@ function toast(message) {
231
248
  * @param require - the module system's synchronous require.
232
249
  * @returns the plugin's `apply`.
233
250
  */
234
- function createPlugin(require) {
251
+ function createPlugin(require: (id: string) => unknown): { apply: (ctx: BrowserContext) => void } {
235
252
  /**
236
253
  * Resolve the pasted folders and put their paths in the composer.
237
254
  * @param root - the composer's editable root.
238
255
  * @param directories - the pasted directory basenames.
239
256
  * @param files - the non-directory files from the same paste.
240
257
  */
241
- async function resolveAndInsert(root, directories, files) {
242
- var result
258
+ async function resolveAndInsert(root: HTMLElement, directories: string[], files: File[]): Promise<void> {
259
+ var result: HostAnswer
243
260
  try {
244
261
  result = await requestPaths(directories)
245
262
  } catch (error) {
@@ -262,8 +279,8 @@ function createPlugin(require) {
262
279
  * Capture-phase paste handler: intercepts directory pastes only.
263
280
  * @param event - the paste event.
264
281
  */
265
- function onPaste(event) {
266
- if (event[SYNTHETIC] === true) return
282
+ function onPaste(event: ClipboardEvent): void {
283
+ if ((event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] === true) return
267
284
  if (!event.clipboardData) return
268
285
 
269
286
  var payload = classifyPayload(event.clipboardData)
@@ -281,7 +298,7 @@ function createPlugin(require) {
281
298
  * Mount the listener; the returned plugin is applied by the client runner.
282
299
  * @param ctx - browser Cordis context.
283
300
  */
284
- function apply(ctx) {
301
+ function apply(ctx: BrowserContext): void {
285
302
  document.addEventListener('paste', onPaste, true)
286
303
  ctx.effect(
287
304
  function () {