From: Eduardo San Martin Morote Date: Fri, 25 Jun 2021 16:19:58 +0000 (+0200) Subject: perf: use esm version of file-saver X-Git-Tag: v2.0.0-beta.5~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=49d1e38a808edbf58970ec2d47a1342d9a5229a1;p=thirdparty%2Fvuejs%2Fpinia.git perf: use esm version of file-saver --- diff --git a/package.json b/package.json index b0a59dca..b2cec955 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/devtools/actions.ts b/src/devtools/actions.ts index 2968389f..43f4c388 100644 --- a/src/devtools/actions.ts +++ b/src/devtools/actions.ts @@ -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 index 00000000..16997054 --- /dev/null +++ b/src/devtools/file-saver.ts @@ -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 + } +} diff --git a/src/types.ts b/src/types.ts index 0141e571..456e3e37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,7 @@ export function isPlainObject( export type DeepPartial = { [K in keyof T]?: DeepPartial } // type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } +// TODO: can we change these to numbers? /** * Possible types for SubscriptionCallback */ diff --git a/yarn.lock b/yarn.lock index 5f96fda9..53fd2d30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,11 +874,6 @@ 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"