From 40d67065baa5c7662391d18780e1db019e63cde1 Mon Sep 17 00:00:00 2001 From: Jeremy Wharton Date: Sat, 11 Nov 2023 15:59:59 -0600 Subject: [PATCH] web/satellite: statically serve Papa Parse worker Papa Parse, the library we use to parse CSV files in the satellite UI, uses a blob URL for its worker. This isn't allowed by our content security policy, so this change implements a Vite plugin that writes the worker code to a file that is statically served. Change-Id: I0ce58c37b86953a71b7433b789b72fbd8ede313d --- .../browser/galleryView/CSVFilePreview.vue | 35 +++++----- web/satellite/src/main.ts | 6 ++ web/satellite/vite.config-vuetify.js | 2 + web/satellite/vite.config.js | 3 + .../vitePlugins/papaParseWorker/index.ts | 67 +++++++++++++++++++ .../vitePlugins/papaParseWorker/module.d.ts | 7 ++ .../vitePlugins/vuetifyThemeCSS/index.ts | 10 +-- .../vitePlugins/vuetifyThemeCSS/module.d.ts | 3 +- .../filePreviewComponents/CSVFilePreview.vue | 35 +++++----- web/satellite/vuetify-poc/src/main.ts | 6 ++ .../vuetify-poc/src/plugins/index.ts | 4 +- 11 files changed, 140 insertions(+), 38 deletions(-) create mode 100644 web/satellite/vitePlugins/papaParseWorker/index.ts create mode 100644 web/satellite/vitePlugins/papaParseWorker/module.d.ts diff --git a/web/satellite/src/components/browser/galleryView/CSVFilePreview.vue b/web/satellite/src/components/browser/galleryView/CSVFilePreview.vue index fda49c80b825..969b2427120d 100644 --- a/web/satellite/src/components/browser/galleryView/CSVFilePreview.vue +++ b/web/satellite/src/components/browser/galleryView/CSVFilePreview.vue @@ -35,21 +35,26 @@ const isLoading = ref(true); const isError = ref(false); onMounted(() => { - Papa.parse(props.src, { - download: true, - worker: true, - header: false, - skipEmptyLines: true, - complete: (results: ParseResult) => { - if (results) items.value = results.data; - isLoading.value = false; - }, - error: (error: Error) => { - if (isError.value) return; - notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); - isError.value = true; - }, - }); + try { + Papa.parse(props.src, { + download: true, + worker: true, + header: false, + skipEmptyLines: true, + complete: (results: ParseResult) => { + if (results) items.value = results.data; + isLoading.value = false; + }, + error: (error: Error) => { + if (isError.value) return; + notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); + isError.value = true; + }, + }); + } catch (error) { + notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); + isError.value = true; + } }); diff --git a/web/satellite/src/main.ts b/web/satellite/src/main.ts index c416554888f5..4b440baa929b 100644 --- a/web/satellite/src/main.ts +++ b/web/satellite/src/main.ts @@ -3,6 +3,8 @@ import { createApp } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; +import Papa from 'papaparse'; +import PAPA_PARSE_WORKER_URL from 'virtual:papa-parse-worker'; import App from './App.vue'; import { router } from './router'; @@ -66,3 +68,7 @@ app.directive('number', { }); app.mount('#app'); + +// By default, Papa Parse uses a blob URL for loading its worker. +// This isn't supported by our content security policy, so we override the URL. +Object.assign(Papa, { BLOB_URL: PAPA_PARSE_WORKER_URL }); diff --git a/web/satellite/vite.config-vuetify.js b/web/satellite/vite.config-vuetify.js index c1d359da33b8..ae242bea710a 100644 --- a/web/satellite/vite.config-vuetify.js +++ b/web/satellite/vite.config-vuetify.js @@ -8,6 +8,7 @@ import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'; import { defineConfig } from 'vite'; import vuetifyThemeCSS from './vitePlugins/vuetifyThemeCSS'; +import papaParseWorker from './vitePlugins/papaParseWorker'; // https://vitejs.dev/config/ export default defineConfig({ @@ -24,6 +25,7 @@ export default defineConfig({ }, }), vuetifyThemeCSS(), + papaParseWorker(), ], define: { 'process.env': {}, diff --git a/web/satellite/vite.config.js b/web/satellite/vite.config.js index 50a6763a9d26..ba487441cb99 100644 --- a/web/satellite/vite.config.js +++ b/web/satellite/vite.config.js @@ -10,6 +10,8 @@ import viteCompression from 'vite-plugin-compression'; import vitePluginRequire from 'vite-plugin-require'; import svgLoader from 'vite-svg-loader'; +import papaParseWorker from './vitePlugins/papaParseWorker'; + const productionBrotliExtensions = ['js', 'css', 'ttf', 'woff', 'woff2']; const plugins = [ @@ -20,6 +22,7 @@ const plugins = [ }, }), vitePluginRequire.default(), + papaParseWorker(), ]; if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) { diff --git a/web/satellite/vitePlugins/papaParseWorker/index.ts b/web/satellite/vitePlugins/papaParseWorker/index.ts new file mode 100644 index 000000000000..49435b19289b --- /dev/null +++ b/web/satellite/vitePlugins/papaParseWorker/index.ts @@ -0,0 +1,67 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +import { Plugin } from 'vite'; +import { build } from 'esbuild'; + +export default function papaParseWorker(): Plugin { + const name = 'papa-parse-worker'; + const virtualModuleId = 'virtual:' + name; + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + let refId = ''; + + return { + name, + + async buildStart() { + // Trick Papa Parse into thinking it's being imported by RequireJS + // so we can capture the AMD callback. + let factory: (() => unknown) | undefined; + global.define = (_: unknown, callback: () => void) => { + factory = callback; + }; + global.define.amd = true; + await import('papaparse'); + delete global.define; + + if (!factory) { + throw new Error('Failed to capture Papa Parse AMD callback'); + } + + const workerCode = ` + var global = (function() { + if (typeof self !== 'undefined') { return self; } + if (typeof window !== 'undefined') { return window; } + if (typeof global !== 'undefined') { return global; } + return {}; + })(); + global.IS_PAPA_WORKER = true; + (${factory.toString()})();`; + + const result = await build({ + stdin: { + contents: workerCode, + }, + write: false, + minify: true, + }); + + refId = this.emitFile({ + type: 'asset', + name: `papaparse-worker.js`, + source: result.outputFiles[0].text, + }); + }, + + resolveId(id: string) { + if (id === virtualModuleId) return resolvedVirtualModuleId; + }, + + load(id: string) { + if (id === resolvedVirtualModuleId) { + return `export default '__VITE_ASSET__${refId}__';`; + } + }, + }; +} diff --git a/web/satellite/vitePlugins/papaParseWorker/module.d.ts b/web/satellite/vitePlugins/papaParseWorker/module.d.ts new file mode 100644 index 000000000000..b09bf17e104d --- /dev/null +++ b/web/satellite/vitePlugins/papaParseWorker/module.d.ts @@ -0,0 +1,7 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +declare module 'virtual:papa-parse-worker' { + const url: string; + export default url; +} diff --git a/web/satellite/vitePlugins/vuetifyThemeCSS/index.ts b/web/satellite/vitePlugins/vuetifyThemeCSS/index.ts index dfc4053d753d..98e02f3e2fc8 100644 --- a/web/satellite/vitePlugins/vuetifyThemeCSS/index.ts +++ b/web/satellite/vitePlugins/vuetifyThemeCSS/index.ts @@ -13,7 +13,7 @@ export default function vuetifyThemeCSS(): Plugin { const resolvedVirtualModuleId = '\0' + virtualModuleId; const theme = createVuetify({ theme: THEME_OPTIONS }).theme; - const themeURLs: Record = {}; + const refIds: Record = {}; return { name, @@ -36,7 +36,7 @@ export default function vuetifyThemeCSS(): Plugin { name: `theme-${name}.css`, source: result.outputFiles[0].text, }); - themeURLs[name] = `__VITE_ASSET__${refId}__`; + refIds[name] = refId; } }, @@ -46,9 +46,9 @@ export default function vuetifyThemeCSS(): Plugin { load(id: string) { if (id === resolvedVirtualModuleId) { - return `export const themeURLs = {${ - Object.entries(themeURLs) - .map(([name, url]) => `'${name}':'${url}'`) + return `export default {${ + Object.entries(refIds) + .map(([name, refId]) => `'${name}':'__VITE_ASSET__${refId}__'`) .join(',') }};`; } diff --git a/web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts b/web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts index cc55b896de4d..9ffe1a63dc82 100644 --- a/web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts +++ b/web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts @@ -2,5 +2,6 @@ // See LICENSE for copying information. declare module 'virtual:vuetify-theme-css' { - export const themeURLs: Record; + const themeURLs: Record; + export default themeURLs; } diff --git a/web/satellite/vuetify-poc/src/components/dialogs/filePreviewComponents/CSVFilePreview.vue b/web/satellite/vuetify-poc/src/components/dialogs/filePreviewComponents/CSVFilePreview.vue index f6f241c36a17..4f9e7dfbce5b 100644 --- a/web/satellite/vuetify-poc/src/components/dialogs/filePreviewComponents/CSVFilePreview.vue +++ b/web/satellite/vuetify-poc/src/components/dialogs/filePreviewComponents/CSVFilePreview.vue @@ -38,21 +38,26 @@ const isLoading = ref(true); const isError = ref(false); onMounted(() => { - Papa.parse(props.src, { - download: true, - worker: true, - header: false, - skipEmptyLines: true, - complete: (results: ParseResult) => { - if (results) items.value = results.data; - isLoading.value = false; - }, - error: (error: Error) => { - if (isError.value) return; - notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); - isError.value = true; - }, - }); + try { + Papa.parse(props.src, { + download: true, + worker: true, + header: false, + skipEmptyLines: true, + complete: (results: ParseResult) => { + if (results) items.value = results.data; + isLoading.value = false; + }, + error: (error: Error) => { + if (isError.value) return; + notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); + isError.value = true; + }, + }); + } catch (error) { + notify.error(`Error parsing object. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW); + isError.value = true; + } }); diff --git a/web/satellite/vuetify-poc/src/main.ts b/web/satellite/vuetify-poc/src/main.ts index cb54c7a05c75..8c582f56fb61 100644 --- a/web/satellite/vuetify-poc/src/main.ts +++ b/web/satellite/vuetify-poc/src/main.ts @@ -8,6 +8,8 @@ */ // Components import { createApp } from 'vue'; +import Papa from 'papaparse'; +import PAPA_PARSE_WORKER_URL from 'virtual:papa-parse-worker'; import App from './App.vue'; @@ -19,3 +21,7 @@ const app = createApp(App); registerPlugins(app); app.mount('#app'); + +// By default, Papa Parse uses a blob URL for loading its worker. +// This isn't supported by our content security policy, so we override the URL. +Object.assign(Papa, { BLOB_URL: PAPA_PARSE_WORKER_URL }); diff --git a/web/satellite/vuetify-poc/src/plugins/index.ts b/web/satellite/vuetify-poc/src/plugins/index.ts index a571d0655754..178af7d31b93 100644 --- a/web/satellite/vuetify-poc/src/plugins/index.ts +++ b/web/satellite/vuetify-poc/src/plugins/index.ts @@ -10,7 +10,7 @@ // Plugins import { App, watch } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; -import { themeURLs } from 'virtual:vuetify-theme-css'; +import THEME_URLS from 'virtual:vuetify-theme-css'; import { router, startTitleWatcher } from '../router'; @@ -35,7 +35,7 @@ function setupTheme() { const themeLinks: Record = {}; - for (const [name, url] of Object.entries(themeURLs)) { + for (const [name, url] of Object.entries(THEME_URLS)) { let link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url;