]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
perf: use esm version of file-saver
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jun 2021 16:19:58 +0000 (18:19 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jun 2021 16:39:49 +0000 (18:39 +0200)
package.json
src/devtools/actions.ts
src/devtools/file-saver.ts [new file with mode: 0644]
src/types.ts
yarn.lock

index b0a59dca2afbff7eb834bb5653af42cfe772af07..b2cec95510cf88c33aaaa8c0dac1ac14449dc9c4 100644 (file)
@@ -68,7 +68,6 @@
     "@rollup/plugin-node-resolve": "^13.0.0",
     "@rollup/plugin-replace": "^2.4.2",
     "@sucrase/jest-plugin": "^2.1.0",
-    "@types/file-saver": "^2.0.2",
     "@types/jest": "^26.0.23",
     "@types/node": "^15.9.0",
     "@vue/server-renderer": "^3.1.1",
@@ -77,7 +76,6 @@
     "brotli": "^1.3.2",
     "codecov": "^3.8.2",
     "conventional-changelog-cli": "^2.1.1",
-    "file-saver": "^2.0.5",
     "jest": "^26.6.3",
     "jest-mock-warn": "^1.1.0",
     "lint-staged": "^11.0.0",
index 2968389fe48af537ec62bf0a6a245395641bf846..43f4c388e4c4c8d78a82f735bd6d0e16b51390eb 100644 (file)
@@ -1,5 +1,5 @@
 import { Pinia } from '../rootStore'
-import { saveAs } from 'file-saver'
+import { saveAs } from './file-saver'
 import { toastMessage } from './utils'
 
 export function checkClipboardAccess() {
diff --git a/src/devtools/file-saver.ts b/src/devtools/file-saver.ts
new file mode 100644 (file)
index 0000000..1699705
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * FileSaver.js A saveAs() FileSaver implementation.
+ *
+ * Originally by Eli Grey, adapted as an ESM module by Eduardo San Martin
+ * Morote.
+ *
+ * License : MIT
+ */
+
+import { IS_CLIENT } from '../env'
+
+// The one and only way of getting global scope in all environments
+// https://stackoverflow.com/q/3277182/1008999
+const _global = /*#__PURE__*/ (() =>
+  typeof window === 'object' && window.window === window
+    ? window
+    : typeof self === 'object' && self.self === self
+    ? self
+    : typeof global === 'object' && global.global === global
+    ? global
+    : typeof globalThis === 'object'
+    ? globalThis
+    : { HTMLElement: null })()
+
+export interface Options {
+  autoBom?: boolean
+}
+
+function bom(blob: Blob, { autoBom = false }: Options = {}) {
+  // prepend BOM for UTF-8 XML and text/* types (including HTML)
+  // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
+  if (
+    autoBom &&
+    /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(
+      blob.type
+    )
+  ) {
+    return new Blob([String.fromCharCode(0xfeff), blob], { type: blob.type })
+  }
+  return blob
+}
+
+function download(url: string, name: string, opts?: Options) {
+  const xhr = new XMLHttpRequest()
+  xhr.open('GET', url)
+  xhr.responseType = 'blob'
+  xhr.onload = function () {
+    saveAs(xhr.response, name, opts)
+  }
+  xhr.onerror = function () {
+    console.error('could not download file')
+  }
+  xhr.send()
+}
+
+function corsEnabled(url: string) {
+  const xhr = new XMLHttpRequest()
+  // use sync to avoid popup blocker
+  xhr.open('HEAD', url, false)
+  try {
+    xhr.send()
+  } catch (e) {}
+  return xhr.status >= 200 && xhr.status <= 299
+}
+
+// `a.click()` doesn't work for all browsers (#465)
+function click(node: Element) {
+  try {
+    node.dispatchEvent(new MouseEvent('click'))
+  } catch (e) {
+    const evt = document.createEvent('MouseEvents')
+    evt.initMouseEvent(
+      'click',
+      true,
+      true,
+      window,
+      0,
+      0,
+      0,
+      80,
+      20,
+      false,
+      false,
+      false,
+      false,
+      0,
+      null
+    )
+    node.dispatchEvent(evt)
+  }
+}
+
+// Detect WebView inside a native macOS app by ruling out all browsers
+// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
+// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
+const isMacOSWebView = /*#__PURE__*/ (() =>
+  /Macintosh/.test(navigator.userAgent) &&
+  /AppleWebKit/.test(navigator.userAgent) &&
+  !/Safari/.test(navigator.userAgent))()
+
+export type SaveAs =
+  | ((blob: Blob, name?: string, opts?: Options) => void)
+  | ((
+      blob: Blob,
+      name: string,
+      opts?: Options | undefined,
+      popup?: Window | null | undefined
+    ) => void)
+
+export const saveAs: SaveAs = !IS_CLIENT
+  ? () => {} // noop
+  : // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
+  'download' in HTMLAnchorElement.prototype && !isMacOSWebView
+  ? downloadSaveAs
+  : // Use msSaveOrOpenBlob as a second approach
+  'msSaveOrOpenBlob' in navigator
+  ? msSaveAs
+  : // Fallback to using FileReader and a popup
+    fileSaverSaveAs
+
+function downloadSaveAs(blob: Blob, name: string = 'download', opts?: Options) {
+  const a = document.createElement('a')
+
+  a.download = name
+  a.rel = 'noopener' // tabnabbing
+
+  // TODO: detect chrome extensions & packaged apps
+  // a.target = '_blank'
+
+  if (typeof blob === 'string') {
+    // Support regular links
+    a.href = blob
+    if (a.origin !== location.origin) {
+      if (corsEnabled(a.href)) {
+        download(blob, name, opts)
+      } else {
+        a.target = '_blank'
+        click(a)
+      }
+    } else {
+      click(a)
+    }
+  } else {
+    // Support blobs
+    a.href = URL.createObjectURL(blob)
+    setTimeout(function () {
+      URL.revokeObjectURL(a.href)
+    }, 4e4) // 40s
+    setTimeout(function () {
+      click(a)
+    }, 0)
+  }
+}
+
+function msSaveAs(blob: Blob, name: string = 'download', opts?: Options) {
+  if (typeof blob === 'string') {
+    if (corsEnabled(blob)) {
+      download(blob, name, opts)
+    } else {
+      const a = document.createElement('a')
+      a.href = blob
+      a.target = '_blank'
+      setTimeout(function () {
+        click(a)
+      })
+    }
+  } else {
+    navigator.msSaveOrOpenBlob(bom(blob, opts), name)
+  }
+}
+
+function fileSaverSaveAs(
+  blob: Blob,
+  name: string,
+  opts?: Options,
+  popup?: Window | null
+) {
+  // Open a popup immediately do go around popup blocker
+  // Mostly only available on user interaction and the fileReader is async so...
+  popup = popup || open('', '_blank')
+  if (popup) {
+    popup.document.title = popup.document.body.innerText = 'downloading...'
+  }
+
+  if (typeof blob === 'string') return download(blob, name, opts)
+
+  const force = blob.type === 'application/octet-stream'
+  const isSafari =
+    /constructor/i.test(String(_global.HTMLElement)) || 'safari' in _global
+  const isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
+
+  if (
+    (isChromeIOS || (force && isSafari) || isMacOSWebView) &&
+    typeof FileReader !== 'undefined'
+  ) {
+    // Safari doesn't allow downloading of blob URLs
+    const reader = new FileReader()
+    reader.onloadend = function () {
+      let url = reader.result
+      if (typeof url !== 'string') {
+        popup = null
+        throw new Error('Wrong reader.result type')
+      }
+      url = isChromeIOS
+        ? url
+        : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
+      if (popup) {
+        popup.location.href = url
+      } else {
+        location.assign(url)
+      }
+      popup = null // reverse-tabnabbing #460
+    }
+    reader.readAsDataURL(blob)
+  } else {
+    const url = URL.createObjectURL(blob)
+    if (popup) popup.location.assign(url)
+    else location.href = url
+    popup = null // reverse-tabnabbing #460
+    setTimeout(function () {
+      URL.revokeObjectURL(url)
+    }, 4e4) // 40s
+  }
+}
index 0141e5713f31105a4ac49665f0da69c8b6e2e0e3..456e3e37daa6554c2ac73b34dc85f21d03f60856 100644 (file)
@@ -29,6 +29,7 @@ export function isPlainObject(
 export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }
 // type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
 
+// TODO: can we change these to numbers?
 /**
  * Possible types for SubscriptionCallback
  */
index 5f96fda9db602dace68411bc808a9507072c2d19..53fd2d30ab72e4b674f87414e031319cb3624b0a 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
-"@types/file-saver@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.2.tgz#bd593ccfaee42ff94a5c1c83bf69ae9be83493b9"
-  integrity sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==
-
 "@types/graceful-fs@^4.1.2":
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@@ -2495,11 +2490,6 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "2.1.1"
 
-file-saver@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
-  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
-
 fill-range@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"