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
This commit is contained in:
Jeremy Wharton 2023-11-11 15:59:59 -06:00 committed by Storj Robot
parent 9930b86791
commit 40d67065ba
11 changed files with 140 additions and 38 deletions

View File

@ -35,21 +35,26 @@ const isLoading = ref<boolean>(true);
const isError = ref<boolean>(false);
onMounted(() => {
Papa.parse(props.src, {
download: true,
worker: true,
header: false,
skipEmptyLines: true,
complete: (results: ParseResult<string[]>) => {
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<string[]>) => {
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;
}
});
</script>

View File

@ -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 });

View File

@ -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': {},

View File

@ -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']) {

View File

@ -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}__';`;
}
},
};
}

View File

@ -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;
}

View File

@ -13,7 +13,7 @@ export default function vuetifyThemeCSS(): Plugin {
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const theme = createVuetify({ theme: THEME_OPTIONS }).theme;
const themeURLs: Record<string, string> = {};
const refIds: Record<string, string> = {};
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(',')
}};`;
}

View File

@ -2,5 +2,6 @@
// See LICENSE for copying information.
declare module 'virtual:vuetify-theme-css' {
export const themeURLs: Record<string, string>;
const themeURLs: Record<string, string>;
export default themeURLs;
}

View File

@ -38,21 +38,26 @@ const isLoading = ref<boolean>(true);
const isError = ref<boolean>(false);
onMounted(() => {
Papa.parse(props.src, {
download: true,
worker: true,
header: false,
skipEmptyLines: true,
complete: (results: ParseResult<string[]>) => {
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<string[]>) => {
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;
}
});
</script>

View File

@ -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 });

View File

@ -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<string, HTMLLinkElement> = {};
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;