Skip to content

Commit

Permalink
web/satellite: statically serve Papa Parse worker
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jewharton authored and Storj Robot committed Nov 14, 2023
1 parent 9930b86 commit 40d6706
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 38 deletions.
35 changes: 20 additions & 15 deletions web/satellite/src/components/browser/galleryView/CSVFilePreview.vue
Expand Up @@ -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>

Expand Down
6 changes: 6 additions & 0 deletions web/satellite/src/main.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
2 changes: 2 additions & 0 deletions web/satellite/vite.config-vuetify.js
Expand Up @@ -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({
Expand All @@ -24,6 +25,7 @@ export default defineConfig({
},
}),
vuetifyThemeCSS(),
papaParseWorker(),
],
define: {
'process.env': {},
Expand Down
3 changes: 3 additions & 0 deletions web/satellite/vite.config.js
Expand Up @@ -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 = [
Expand All @@ -20,6 +22,7 @@ const plugins = [
},
}),
vitePluginRequire.default(),
papaParseWorker(),
];

if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
Expand Down
67 changes: 67 additions & 0 deletions 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}__';`;
}
},
};
}
7 changes: 7 additions & 0 deletions 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;
}
10 changes: 5 additions & 5 deletions web/satellite/vitePlugins/vuetifyThemeCSS/index.ts
Expand Up @@ -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,
Expand All @@ -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;
}
},

Expand All @@ -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(',')
}};`;
}
Expand Down
3 changes: 2 additions & 1 deletion web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts
Expand Up @@ -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;
}
Expand Up @@ -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>

Expand Down
6 changes: 6 additions & 0 deletions web/satellite/vuetify-poc/src/main.ts
Expand Up @@ -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';

Expand All @@ -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 });
4 changes: 2 additions & 2 deletions web/satellite/vuetify-poc/src/plugins/index.ts
Expand Up @@ -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';

Expand All @@ -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;
Expand Down

0 comments on commit 40d6706

Please sign in to comment.