@destyler/file-upload 0.0.7-beta.7 → 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,550 @@
1
+ import { createAnatomy } from "@destyler/anatomy";
2
+ import { addDomEvent, contains, createScope, dataAttr, getEventTarget, isSelfTarget, raf, visuallyHiddenStyle } from "@destyler/dom";
3
+ import { formatBytes } from "@destyler/i18n";
4
+ import { getAcceptAttrString, isFileEqual, isValidFileSize, isValidFileType } from "@destyler/file";
5
+ import { callAll, compact, createSplitProps } from "@destyler/utils";
6
+ import { createMachine, ref } from "@destyler/xstate";
7
+ import { createProps } from "@destyler/types";
8
+
9
+ //#region src/anatomy.ts
10
+ const anatomy = createAnatomy("file-upload").parts("root", "dropzone", "item", "itemDeleteTrigger", "itemGroup", "itemName", "itemPreview", "itemPreviewImage", "itemSizeText", "label", "trigger", "clearTrigger");
11
+ const parts = anatomy.build();
12
+
13
+ //#endregion
14
+ //#region src/dom.ts
15
+ const dom = createScope({
16
+ getRootId: (ctx) => ctx.ids?.root ?? `file-upload:${ctx.id}`,
17
+ getDropzoneId: (ctx) => ctx.ids?.dropzone ?? `file-upload:${ctx.id}:dropzone`,
18
+ getHiddenInputId: (ctx) => ctx.ids?.hiddenInput ?? `file-upload:${ctx.id}:input`,
19
+ getTriggerId: (ctx) => ctx.ids?.trigger ?? `file-upload:${ctx.id}:trigger`,
20
+ getLabelId: (ctx) => ctx.ids?.label ?? `file-upload:${ctx.id}:label`,
21
+ getItemId: (ctx, id) => ctx.ids?.item?.(id) ?? `file-upload:${ctx.id}:item:${id}`,
22
+ getItemNameId: (ctx, id) => ctx.ids?.itemName?.(id) ?? `file-upload:${ctx.id}:item-name:${id}`,
23
+ getItemSizeTextId: (ctx, id) => ctx.ids?.itemSizeText?.(id) ?? `file-upload:${ctx.id}:item-size:${id}`,
24
+ getItemPreviewId: (ctx, id) => ctx.ids?.itemPreview?.(id) ?? `file-upload:${ctx.id}:item-preview:${id}`,
25
+ getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
26
+ getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
27
+ getDropzoneEl: (ctx) => dom.getById(ctx, dom.getDropzoneId(ctx))
28
+ });
29
+
30
+ //#endregion
31
+ //#region src/utils.ts
32
+ function isEventWithFiles(event) {
33
+ const target = getEventTarget(event);
34
+ if (!event.dataTransfer) return !!target && "files" in target;
35
+ return event.dataTransfer.types.some((type) => {
36
+ return type === "Files" || type === "application/x-moz-file";
37
+ });
38
+ }
39
+ function isFilesWithinRange(ctx, incomingCount) {
40
+ if (!ctx.multiple && incomingCount > 1) return false;
41
+ if (!ctx.multiple && incomingCount + ctx.acceptedFiles.length === 2) return true;
42
+ if (incomingCount + ctx.acceptedFiles.length > ctx.maxFiles) return false;
43
+ return true;
44
+ }
45
+ function getFilesFromEvent(ctx, files) {
46
+ const acceptedFiles = [];
47
+ const rejectedFiles = [];
48
+ files.forEach((file) => {
49
+ const [accepted, acceptError] = isValidFileType(file, ctx.acceptAttr);
50
+ const [sizeMatch, sizeError] = isValidFileSize(file, ctx.minFileSize, ctx.maxFileSize);
51
+ const validateErrors = ctx.validate?.(file, {
52
+ acceptedFiles: ctx.acceptedFiles,
53
+ rejectedFiles: ctx.rejectedFiles
54
+ });
55
+ const valid = validateErrors ? validateErrors.length === 0 : true;
56
+ if (accepted && sizeMatch && valid) acceptedFiles.push(file);
57
+ else {
58
+ const errors = [acceptError, sizeError];
59
+ if (!valid) errors.push(...validateErrors ?? []);
60
+ rejectedFiles.push({
61
+ file,
62
+ errors: errors.filter(Boolean)
63
+ });
64
+ }
65
+ });
66
+ if (!isFilesWithinRange(ctx, acceptedFiles.length)) {
67
+ acceptedFiles.forEach((file) => {
68
+ rejectedFiles.push({
69
+ file,
70
+ errors: ["TOO_MANY_FILES"]
71
+ });
72
+ });
73
+ acceptedFiles.splice(0);
74
+ }
75
+ return {
76
+ acceptedFiles,
77
+ rejectedFiles
78
+ };
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/connect.ts
83
+ function connect(state, send, normalize) {
84
+ const disabled = state.context.disabled;
85
+ const allowDrop = state.context.allowDrop;
86
+ const translations = state.context.translations;
87
+ const dragging = state.matches("dragging");
88
+ const focused = state.matches("focused") && !disabled;
89
+ return {
90
+ dragging,
91
+ focused,
92
+ disabled: !!disabled,
93
+ openFilePicker() {
94
+ if (disabled) return;
95
+ send("OPEN");
96
+ },
97
+ deleteFile(file) {
98
+ send({
99
+ type: "FILE.DELETE",
100
+ file
101
+ });
102
+ },
103
+ acceptedFiles: state.context.acceptedFiles,
104
+ rejectedFiles: state.context.rejectedFiles,
105
+ setFiles(files) {
106
+ const count = files.length;
107
+ send({
108
+ type: "FILES.SET",
109
+ files,
110
+ count
111
+ });
112
+ },
113
+ clearRejectedFiles() {
114
+ send({ type: "REJECTED_FILES.CLEAR" });
115
+ },
116
+ clearFiles() {
117
+ send({ type: "FILES.CLEAR" });
118
+ },
119
+ getFileSize(file) {
120
+ return formatBytes(file.size, state.context.locale);
121
+ },
122
+ createFileUrl(file, cb) {
123
+ const win = dom.getWin(state.context);
124
+ const url = win.URL.createObjectURL(file);
125
+ cb(url);
126
+ return () => win.URL.revokeObjectURL(url);
127
+ },
128
+ setClipboardFiles(dt) {
129
+ if (disabled) return false;
130
+ const items = Array.from(dt?.items ?? []);
131
+ const files = items.reduce((acc, item) => {
132
+ if (item.kind !== "file") return acc;
133
+ const file = item.getAsFile();
134
+ if (!file) return acc;
135
+ return [...acc, file];
136
+ }, []);
137
+ if (!files.length) return false;
138
+ send({
139
+ type: "FILES.SET",
140
+ files
141
+ });
142
+ return true;
143
+ },
144
+ getRootProps() {
145
+ return normalize.element({
146
+ ...parts.root.attrs,
147
+ "dir": state.context.dir,
148
+ "id": dom.getRootId(state.context),
149
+ "data-disabled": dataAttr(disabled),
150
+ "data-dragging": dataAttr(dragging)
151
+ });
152
+ },
153
+ getDropzoneProps(props$1 = {}) {
154
+ return normalize.element({
155
+ ...parts.dropzone.attrs,
156
+ "dir": state.context.dir,
157
+ "id": dom.getDropzoneId(state.context),
158
+ "tabIndex": disabled || props$1.disableClick ? void 0 : 0,
159
+ "role": props$1.disableClick ? "application" : "button",
160
+ "aria-label": translations.dropzone,
161
+ "aria-disabled": disabled,
162
+ "data-invalid": dataAttr(state.context.invalid),
163
+ "data-disabled": dataAttr(disabled),
164
+ "data-dragging": dataAttr(dragging),
165
+ onKeyDown(event) {
166
+ if (disabled) return;
167
+ if (event.defaultPrevented) return;
168
+ if (!isSelfTarget(event)) return;
169
+ if (props$1.disableClick) return;
170
+ if (event.key !== "Enter" && event.key !== " ") return;
171
+ send({
172
+ type: "DROPZONE.CLICK",
173
+ src: "keydown"
174
+ });
175
+ },
176
+ onClick(event) {
177
+ if (disabled) return;
178
+ if (event.defaultPrevented) return;
179
+ if (props$1.disableClick) return;
180
+ if (!isSelfTarget(event)) return;
181
+ if (event.currentTarget.localName === "label") event.preventDefault();
182
+ send("DROPZONE.CLICK");
183
+ },
184
+ onDragOver(event) {
185
+ if (disabled) return;
186
+ if (!allowDrop) return;
187
+ event.preventDefault();
188
+ event.stopPropagation();
189
+ try {
190
+ event.dataTransfer.dropEffect = "copy";
191
+ } catch {}
192
+ const hasFiles = isEventWithFiles(event);
193
+ if (!hasFiles) return;
194
+ const count = event.dataTransfer.items.length;
195
+ send({
196
+ type: "DROPZONE.DRAG_OVER",
197
+ count
198
+ });
199
+ },
200
+ onDragLeave(event) {
201
+ if (disabled) return;
202
+ if (!allowDrop) return;
203
+ if (contains(event.currentTarget, event.relatedTarget)) return;
204
+ send({ type: "DROPZONE.DRAG_LEAVE" });
205
+ },
206
+ onDrop(event) {
207
+ if (disabled) return;
208
+ if (allowDrop) {
209
+ event.preventDefault();
210
+ event.stopPropagation();
211
+ }
212
+ const hasFiles = isEventWithFiles(event);
213
+ if (disabled || !hasFiles) return;
214
+ send({
215
+ type: "DROPZONE.DROP",
216
+ files: Array.from(event.dataTransfer.files)
217
+ });
218
+ },
219
+ onFocus() {
220
+ if (disabled) return;
221
+ send("DROPZONE.FOCUS");
222
+ },
223
+ onBlur() {
224
+ if (disabled) return;
225
+ send("DROPZONE.BLUR");
226
+ }
227
+ });
228
+ },
229
+ getTriggerProps() {
230
+ return normalize.button({
231
+ ...parts.trigger.attrs,
232
+ "dir": state.context.dir,
233
+ "id": dom.getTriggerId(state.context),
234
+ disabled,
235
+ "data-disabled": dataAttr(disabled),
236
+ "data-invalid": dataAttr(state.context.invalid),
237
+ "type": "button",
238
+ onClick(event) {
239
+ if (disabled) return;
240
+ if (contains(dom.getDropzoneEl(state.context), event.currentTarget)) event.stopPropagation();
241
+ send("OPEN");
242
+ }
243
+ });
244
+ },
245
+ getHiddenInputProps() {
246
+ return normalize.input({
247
+ id: dom.getHiddenInputId(state.context),
248
+ tabIndex: -1,
249
+ disabled,
250
+ type: "file",
251
+ required: state.context.required,
252
+ capture: state.context.capture,
253
+ name: state.context.name,
254
+ accept: state.context.acceptAttr,
255
+ webkitdirectory: state.context.directory ? "" : void 0,
256
+ multiple: state.context.multiple || state.context.maxFiles > 1,
257
+ onClick(event) {
258
+ event.stopPropagation();
259
+ event.currentTarget.value = "";
260
+ },
261
+ onInput(event) {
262
+ if (disabled) return;
263
+ const { files } = event.currentTarget;
264
+ send({
265
+ type: "FILES.SET",
266
+ files: files ? Array.from(files) : []
267
+ });
268
+ },
269
+ style: visuallyHiddenStyle
270
+ });
271
+ },
272
+ getItemGroupProps() {
273
+ return normalize.element({
274
+ ...parts.itemGroup.attrs,
275
+ "dir": state.context.dir,
276
+ "data-disabled": dataAttr(disabled)
277
+ });
278
+ },
279
+ getItemProps(props$1) {
280
+ const { file } = props$1;
281
+ return normalize.element({
282
+ ...parts.item.attrs,
283
+ "dir": state.context.dir,
284
+ "id": dom.getItemId(state.context, file.name),
285
+ "data-disabled": dataAttr(disabled)
286
+ });
287
+ },
288
+ getItemNameProps(props$1) {
289
+ const { file } = props$1;
290
+ return normalize.element({
291
+ ...parts.itemName.attrs,
292
+ "dir": state.context.dir,
293
+ "id": dom.getItemNameId(state.context, file.name),
294
+ "data-disabled": dataAttr(disabled)
295
+ });
296
+ },
297
+ getItemSizeTextProps(props$1) {
298
+ const { file } = props$1;
299
+ return normalize.element({
300
+ ...parts.itemSizeText.attrs,
301
+ "dir": state.context.dir,
302
+ "id": dom.getItemSizeTextId(state.context, file.name),
303
+ "data-disabled": dataAttr(disabled)
304
+ });
305
+ },
306
+ getItemPreviewProps(props$1) {
307
+ const { file } = props$1;
308
+ return normalize.element({
309
+ ...parts.itemPreview.attrs,
310
+ "dir": state.context.dir,
311
+ "id": dom.getItemPreviewId(state.context, file.name),
312
+ "data-disabled": dataAttr(disabled)
313
+ });
314
+ },
315
+ getItemPreviewImageProps(props$1) {
316
+ const { file, url } = props$1;
317
+ const isImage = file.type.startsWith("image/");
318
+ if (!isImage) throw new Error("Preview Image is only supported for image files");
319
+ return normalize.img({
320
+ ...parts.itemPreviewImage.attrs,
321
+ "alt": translations.itemPreview?.(file),
322
+ "src": url,
323
+ "data-disabled": dataAttr(disabled)
324
+ });
325
+ },
326
+ getItemDeleteTriggerProps(props$1) {
327
+ const { file } = props$1;
328
+ return normalize.button({
329
+ ...parts.itemDeleteTrigger.attrs,
330
+ "dir": state.context.dir,
331
+ "type": "button",
332
+ disabled,
333
+ "data-disabled": dataAttr(disabled),
334
+ "aria-label": translations.deleteFile?.(file),
335
+ onClick() {
336
+ if (disabled) return;
337
+ send({
338
+ type: "FILE.DELETE",
339
+ file
340
+ });
341
+ }
342
+ });
343
+ },
344
+ getLabelProps() {
345
+ return normalize.label({
346
+ ...parts.label.attrs,
347
+ "dir": state.context.dir,
348
+ "id": dom.getLabelId(state.context),
349
+ "htmlFor": dom.getHiddenInputId(state.context),
350
+ "data-disabled": dataAttr(disabled)
351
+ });
352
+ },
353
+ getClearTriggerProps() {
354
+ return normalize.button({
355
+ ...parts.clearTrigger.attrs,
356
+ "dir": state.context.dir,
357
+ "type": "button",
358
+ disabled,
359
+ "hidden": state.context.acceptedFiles.length === 0,
360
+ "data-disabled": dataAttr(disabled),
361
+ onClick(event) {
362
+ if (event.defaultPrevented) return;
363
+ if (disabled) return;
364
+ send({ type: "FILES.CLEAR" });
365
+ }
366
+ });
367
+ }
368
+ };
369
+ }
370
+
371
+ //#endregion
372
+ //#region src/machine.ts
373
+ const invoke = {
374
+ change: (ctx) => {
375
+ ctx.onFileChange?.({
376
+ acceptedFiles: ctx.acceptedFiles,
377
+ rejectedFiles: ctx.rejectedFiles
378
+ });
379
+ },
380
+ accept: (ctx) => {
381
+ ctx.onFileAccept?.({ files: ctx.acceptedFiles });
382
+ },
383
+ reject: (ctx) => {
384
+ ctx.onFileReject?.({ files: ctx.rejectedFiles });
385
+ }
386
+ };
387
+ const set = { files: (ctx, acceptedFiles, rejectedFiles) => {
388
+ ctx.acceptedFiles = ref(acceptedFiles);
389
+ invoke.accept(ctx);
390
+ if (rejectedFiles) {
391
+ ctx.rejectedFiles = ref(rejectedFiles);
392
+ invoke.reject(ctx);
393
+ }
394
+ invoke.change(ctx);
395
+ } };
396
+ function machine(userContext) {
397
+ const ctx = compact(userContext);
398
+ return createMachine({
399
+ id: "fileupload",
400
+ initial: "idle",
401
+ context: {
402
+ minFileSize: 0,
403
+ maxFileSize: Number.POSITIVE_INFINITY,
404
+ maxFiles: 1,
405
+ allowDrop: true,
406
+ accept: ctx.accept,
407
+ preventDocumentDrop: true,
408
+ ...ctx,
409
+ acceptedFiles: ref([]),
410
+ rejectedFiles: ref([]),
411
+ translations: {
412
+ dropzone: "dropzone",
413
+ itemPreview: (file) => `preview of ${file.name}`,
414
+ deleteFile: (file) => `delete file ${file.name}`,
415
+ ...ctx.translations
416
+ }
417
+ },
418
+ computed: {
419
+ acceptAttr: (ctx$1) => getAcceptAttrString(ctx$1.accept),
420
+ multiple: (ctx$1) => ctx$1.maxFiles > 1
421
+ },
422
+ watch: { acceptedFiles: ["syncInputElement"] },
423
+ on: {
424
+ "FILES.SET": { actions: ["setFilesFromEvent"] },
425
+ "FILE.DELETE": { actions: ["removeFile"] },
426
+ "FILES.CLEAR": { actions: ["clearFiles"] },
427
+ "REJECTED_FILES.CLEAR": { actions: ["clearRejectedFiles"] }
428
+ },
429
+ activities: ["preventDocumentDrop"],
430
+ states: {
431
+ idle: { on: {
432
+ "OPEN": { actions: ["openFilePicker"] },
433
+ "DROPZONE.CLICK": { actions: ["openFilePicker"] },
434
+ "DROPZONE.FOCUS": "focused",
435
+ "DROPZONE.DRAG_OVER": "dragging"
436
+ } },
437
+ focused: { on: {
438
+ "DROPZONE.BLUR": "idle",
439
+ "OPEN": { actions: ["openFilePicker"] },
440
+ "DROPZONE.CLICK": { actions: ["openFilePicker"] },
441
+ "DROPZONE.DRAG_OVER": "dragging"
442
+ } },
443
+ dragging: { on: {
444
+ "DROPZONE.DROP": {
445
+ target: "idle",
446
+ actions: ["setFilesFromEvent"]
447
+ },
448
+ "DROPZONE.DRAG_LEAVE": "idle"
449
+ } }
450
+ }
451
+ }, {
452
+ activities: { preventDocumentDrop(ctx$1) {
453
+ if (!ctx$1.preventDocumentDrop) return;
454
+ if (!ctx$1.allowDrop) return;
455
+ if (ctx$1.disabled) return;
456
+ const doc = dom.getDoc(ctx$1);
457
+ const onDragOver = (event) => {
458
+ event?.preventDefault();
459
+ };
460
+ const onDrop = (event) => {
461
+ if (contains(dom.getRootEl(ctx$1), getEventTarget(event))) return;
462
+ event.preventDefault();
463
+ };
464
+ return callAll(addDomEvent(doc, "dragover", onDragOver, false), addDomEvent(doc, "drop", onDrop, false));
465
+ } },
466
+ actions: {
467
+ syncInputElement(ctx$1) {
468
+ queueMicrotask(() => {
469
+ const inputEl = dom.getHiddenInputEl(ctx$1);
470
+ if (!inputEl) return;
471
+ const win = dom.getWin(ctx$1);
472
+ const dataTransfer = new win.DataTransfer();
473
+ ctx$1.acceptedFiles.forEach((v) => {
474
+ dataTransfer.items.add(v);
475
+ });
476
+ inputEl.files = dataTransfer.files;
477
+ inputEl.dispatchEvent(new win.Event("change", { bubbles: true }));
478
+ });
479
+ },
480
+ openFilePicker(ctx$1) {
481
+ raf(() => {
482
+ dom.getHiddenInputEl(ctx$1)?.click();
483
+ });
484
+ },
485
+ setFilesFromEvent(ctx$1, evt) {
486
+ const result = getFilesFromEvent(ctx$1, evt.files);
487
+ const { acceptedFiles, rejectedFiles } = result;
488
+ if (ctx$1.multiple) {
489
+ const files = ref([...ctx$1.acceptedFiles, ...acceptedFiles]);
490
+ set.files(ctx$1, files, rejectedFiles);
491
+ return;
492
+ }
493
+ if (acceptedFiles.length) {
494
+ const files = ref([acceptedFiles[0]]);
495
+ set.files(ctx$1, files, rejectedFiles);
496
+ } else if (rejectedFiles.length) set.files(ctx$1, ctx$1.acceptedFiles, rejectedFiles);
497
+ },
498
+ removeFile(ctx$1, evt) {
499
+ const files = Array.from(ctx$1.acceptedFiles.filter((file) => file !== evt.file));
500
+ const rejectedFiles = Array.from(ctx$1.rejectedFiles.filter((item) => item.file !== evt.file));
501
+ ctx$1.acceptedFiles = ref(files);
502
+ ctx$1.rejectedFiles = ref(rejectedFiles);
503
+ invoke.change(ctx$1);
504
+ },
505
+ clearRejectedFiles(ctx$1) {
506
+ ctx$1.rejectedFiles = ref([]);
507
+ invoke.change(ctx$1);
508
+ },
509
+ clearFiles(ctx$1) {
510
+ ctx$1.acceptedFiles = ref([]);
511
+ ctx$1.rejectedFiles = ref([]);
512
+ invoke.change(ctx$1);
513
+ }
514
+ },
515
+ compareFns: { acceptedFiles: (a, b) => a.length === b.length && a.every((file, i) => isFileEqual(file, b[i])) }
516
+ });
517
+ }
518
+
519
+ //#endregion
520
+ //#region src/props.ts
521
+ const props = createProps()([
522
+ "accept",
523
+ "allowDrop",
524
+ "capture",
525
+ "dir",
526
+ "directory",
527
+ "disabled",
528
+ "getRootNode",
529
+ "id",
530
+ "ids",
531
+ "locale",
532
+ "maxFiles",
533
+ "maxFileSize",
534
+ "minFileSize",
535
+ "name",
536
+ "invalid",
537
+ "onFileAccept",
538
+ "onFileReject",
539
+ "onFileChange",
540
+ "preventDocumentDrop",
541
+ "required",
542
+ "translations",
543
+ "validate"
544
+ ]);
545
+ const splitProps = createSplitProps(props);
546
+ const itemProps = createProps()(["file"]);
547
+ const splitItemProps = createSplitProps(itemProps);
548
+
549
+ //#endregion
550
+ export { anatomy, connect, itemProps, machine, props, splitItemProps, splitProps };
package/package.json CHANGED
@@ -1,37 +1,33 @@
1
1
  {
2
2
  "name": "@destyler/file-upload",
3
3
  "type": "module",
4
- "version": "0.0.7-beta.7",
4
+ "version": "0.1.0",
5
5
  "description": "a component that is used to upload multiple files.",
6
6
  "author": "Elone Hoo <elonehoo@gmail.com>",
7
7
  "license": "MIT",
8
8
  "exports": {
9
- ".": {
10
- "types": "./dist/index.d.mts",
11
- "default": "./dist/index.mjs"
12
- },
13
- "./*": "./*"
9
+ ".": "./dist/index.js"
14
10
  },
15
- "main": "dist/index.cjs",
16
- "module": "dist/index.mjs",
11
+ "main": "dist/index.js",
12
+ "module": "dist/index.js",
17
13
  "types": "dist/index.d.ts",
18
14
  "files": [
19
15
  "*.d.ts",
20
16
  "dist"
21
17
  ],
22
18
  "dependencies": {
23
- "@zag-js/anatomy": "^0.82.1",
24
- "@zag-js/core": "^0.82.1",
25
- "@zag-js/dom-query": "^0.82.1",
26
- "@zag-js/file-utils": "^0.82.1",
27
- "@zag-js/i18n-utils": "^0.82.1",
28
- "@zag-js/types": "^0.82.1",
29
- "@zag-js/utils": "^0.82.1"
19
+ "@destyler/anatomy": "0.1.0",
20
+ "@destyler/dom": "0.1.0",
21
+ "@destyler/file": "0.1.0",
22
+ "@destyler/i18n": "0.1.0",
23
+ "@destyler/types": "0.1.0",
24
+ "@destyler/xstate": "0.1.0",
25
+ "@destyler/utils": "0.1.0"
30
26
  },
31
27
  "scripts": {
32
28
  "clear": "rimraf ./dist",
33
29
  "clear:node_modules": "rimraf ./node_modules",
34
- "build": "pnpm run clear && unbuild",
35
- "stub": "unbuild --stub"
30
+ "build": "pnpm run clear && tsdown",
31
+ "watch": "tsdown --watch"
36
32
  }
37
33
  }