web/satellite: statically serve Vuetify theme styles
Vuetify's way of applying themes uses an inline stylesheet. This is incompatible with our CSP policy, so this change implements a Vite plugin that writes theme styles to CSS files that are statically served. Change-Id: I73e3a032435e46d41248c5181e913a8e04f65881
This commit is contained in:
parent
74757ffc1d
commit
51fefb2882
@ -61,6 +61,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
'newlines-between': 'always',
|
'newlines-between': 'always',
|
||||||
}],
|
}],
|
||||||
|
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
||||||
'no-duplicate-imports': 'error',
|
'no-duplicate-imports': 'error',
|
||||||
'import/default': 'off',
|
'import/default': 'off',
|
||||||
'vue/script-setup-uses-vars': 'error',
|
'vue/script-setup-uses-vars': 'error',
|
@ -2,6 +2,7 @@
|
|||||||
"name": "storj-satellite",
|
"name": "storj-satellite",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.vue",
|
"src/**/*.vue",
|
||||||
"src/types/*.d.ts",
|
"src/types/*.d.ts",
|
||||||
"tests/**/*.ts"
|
"tests/**/*.ts",
|
||||||
|
"vitePlugins/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
@ -7,6 +7,8 @@ import vue from '@vitejs/plugin-vue';
|
|||||||
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
|
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
import vuetifyThemeCSS from './vitePlugins/vuetifyThemeCSS';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/static/dist_vuetify_poc',
|
base: '/static/dist_vuetify_poc',
|
||||||
@ -21,6 +23,7 @@ export default defineConfig({
|
|||||||
configFile: 'vuetify-poc/src/styles/settings.scss',
|
configFile: 'vuetify-poc/src/styles/settings.scss',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
vuetifyThemeCSS(),
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
'process.env': {},
|
'process.env': {},
|
||||||
|
@ -19,7 +19,7 @@ const plugins = [
|
|||||||
plugins: [{ name: 'removeViewBox', fn: () => {} }],
|
plugins: [{ name: 'removeViewBox', fn: () => {} }],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
vitePluginRequire(),
|
vitePluginRequire.default(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
|
if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
|
||||||
|
57
web/satellite/vitePlugins/vuetifyThemeCSS/index.ts
Normal file
57
web/satellite/vitePlugins/vuetifyThemeCSS/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { Plugin } from 'vite';
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
import { createVuetify } from 'vuetify';
|
||||||
|
|
||||||
|
import { THEME_OPTIONS } from '../../vuetify-poc/src/plugins/theme';
|
||||||
|
|
||||||
|
export default function vuetifyThemeCSS(): Plugin {
|
||||||
|
const name = 'vuetify-theme-css';
|
||||||
|
const virtualModuleId = 'virtual:' + name;
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
|
||||||
|
const theme = createVuetify({ theme: THEME_OPTIONS }).theme;
|
||||||
|
const themeURLs: Record<string, string> = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
|
||||||
|
async buildStart() {
|
||||||
|
for (const name of Object.keys(theme.themes.value)) {
|
||||||
|
theme.global.name.value = name;
|
||||||
|
|
||||||
|
const result = await build({
|
||||||
|
stdin: {
|
||||||
|
contents: theme.styles.value,
|
||||||
|
loader: 'css',
|
||||||
|
},
|
||||||
|
write: false,
|
||||||
|
minify: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refId = this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
name: `theme-${name}.css`,
|
||||||
|
source: result.outputFiles[0].text,
|
||||||
|
});
|
||||||
|
themeURLs[name] = `__VITE_ASSET__${refId}__`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveId(id: string) {
|
||||||
|
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||||
|
},
|
||||||
|
|
||||||
|
load(id: string) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
return `export const themeURLs = {${
|
||||||
|
Object.entries(themeURLs)
|
||||||
|
.map(([name, url]) => `'${name}':'${url}'`)
|
||||||
|
.join(',')
|
||||||
|
}};`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
6
web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts
vendored
Normal file
6
web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
declare module 'virtual:vuetify-theme-css' {
|
||||||
|
export const themeURLs: Record<string, string>;
|
||||||
|
}
|
@ -8,8 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
import { App } from 'vue';
|
import { App, watch } from 'vue';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { themeURLs } from 'virtual:vuetify-theme-css';
|
||||||
|
|
||||||
import { router, startTitleWatcher } from '../router';
|
import { router, startTitleWatcher } from '../router';
|
||||||
|
|
||||||
@ -20,7 +21,46 @@ import NotificatorPlugin from '@/utils/plugins/notificator';
|
|||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
// Vuetify's way of applying themes uses a dynamic inline stylesheet.
|
||||||
|
// This is incompatible with our CSP policy, so circumvent it.
|
||||||
|
function setupTheme() {
|
||||||
|
const oldAppend = document.head.appendChild.bind(document.head);
|
||||||
|
document.head.appendChild = function<T extends Node>(node: T): T {
|
||||||
|
if (node instanceof HTMLStyleElement && node.id === 'vuetify-theme-stylesheet') {
|
||||||
|
node.remove();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return oldAppend(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeLinks: Record<string, HTMLLinkElement> = {};
|
||||||
|
|
||||||
|
for (const [name, url] of Object.entries(themeURLs)) {
|
||||||
|
let link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = url;
|
||||||
|
link.disabled = name !== vuetify.theme.global.name.value;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
themeLinks[name] = link;
|
||||||
|
|
||||||
|
// If we don't preload the style, there will be a delay after
|
||||||
|
// toggling to it for the first time.
|
||||||
|
link = document.createElement('link');
|
||||||
|
link.rel = 'preload';
|
||||||
|
link.as = 'style';
|
||||||
|
link.href = url;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => vuetify.theme.global.name.value, newName => {
|
||||||
|
for (const [name, link] of Object.entries(themeLinks)) {
|
||||||
|
link.disabled = name !== newName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerPlugins(app: App<Element>) {
|
export function registerPlugins(app: App<Element>) {
|
||||||
|
setupTheme();
|
||||||
app
|
app
|
||||||
.use(vuetify)
|
.use(vuetify)
|
||||||
.use(router)
|
.use(router)
|
||||||
|
57
web/satellite/vuetify-poc/src/plugins/theme.ts
Normal file
57
web/satellite/vuetify-poc/src/plugins/theme.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { createVuetify } from 'vuetify';
|
||||||
|
|
||||||
|
type ThemeOptions = NonNullable<NonNullable<Parameters<typeof createVuetify>[0]>['theme']>;
|
||||||
|
|
||||||
|
export const THEME_OPTIONS: ThemeOptions = {
|
||||||
|
themes: {
|
||||||
|
light: {
|
||||||
|
colors: {
|
||||||
|
primary: '#0149FF',
|
||||||
|
secondary: '#0218A7',
|
||||||
|
background: '#FFF',
|
||||||
|
surface: '#FFF',
|
||||||
|
info: '#0059D0',
|
||||||
|
help: '#FFA800',
|
||||||
|
success: '#00AC26',
|
||||||
|
warning: '#FF7F00',
|
||||||
|
error: '#FF0149',
|
||||||
|
purple: '#7B61FF',
|
||||||
|
blue6: '#091c45',
|
||||||
|
blue5: '#0218A7',
|
||||||
|
blue4: '#0059D0',
|
||||||
|
blue2: '#003ACD',
|
||||||
|
yellow: '#FFC600',
|
||||||
|
yellow2: '#FFB701',
|
||||||
|
orange: '#FFA800',
|
||||||
|
green: '#00B150',
|
||||||
|
purple2: '#502EFF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
colors: {
|
||||||
|
primary: '#0149FF',
|
||||||
|
secondary: '#537CFF',
|
||||||
|
background: '#090920',
|
||||||
|
success: '#00AC26',
|
||||||
|
help: '#FFC600',
|
||||||
|
error: '#FF0149',
|
||||||
|
surface: '#090920',
|
||||||
|
purple: '#A18EFF',
|
||||||
|
blue6: '#091c45',
|
||||||
|
blue5: '#2196f3',
|
||||||
|
blue4: '#0059D0',
|
||||||
|
blue2: '#003ACD',
|
||||||
|
yellow: '#FFC600',
|
||||||
|
yellow2: '#FFB701',
|
||||||
|
orange: '#FFA800',
|
||||||
|
warning: '#FF8A00',
|
||||||
|
// green: '#00B150',
|
||||||
|
green: '#00e366',
|
||||||
|
purple2: '#A18EFF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -13,62 +13,14 @@ import '@fontsource-variable/inter';
|
|||||||
import { createVuetify } from 'vuetify';
|
import { createVuetify } from 'vuetify';
|
||||||
import { md3 } from 'vuetify/blueprints';
|
import { md3 } from 'vuetify/blueprints';
|
||||||
|
|
||||||
import '../styles/styles.scss';
|
import '@poc/styles/styles.scss';
|
||||||
|
import { THEME_OPTIONS } from '@poc/plugins/theme';
|
||||||
|
|
||||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
export default createVuetify({
|
export default createVuetify({
|
||||||
// Use blueprint for Material Design 3
|
// Use blueprint for Material Design 3
|
||||||
blueprint: md3,
|
blueprint: md3,
|
||||||
theme: {
|
theme: THEME_OPTIONS,
|
||||||
themes: {
|
|
||||||
light: {
|
|
||||||
colors: {
|
|
||||||
primary: '#0149FF',
|
|
||||||
secondary: '#0218A7',
|
|
||||||
background: '#FFF',
|
|
||||||
surface: '#FFF',
|
|
||||||
info: '#0059D0',
|
|
||||||
help: '#FFA800',
|
|
||||||
success: '#00AC26',
|
|
||||||
warning: '#FF7F00',
|
|
||||||
error: '#FF0149',
|
|
||||||
purple: '#7B61FF',
|
|
||||||
blue6: '#091c45',
|
|
||||||
blue5: '#0218A7',
|
|
||||||
blue4: '#0059D0',
|
|
||||||
blue2: '#003ACD',
|
|
||||||
yellow: '#FFC600',
|
|
||||||
yellow2: '#FFB701',
|
|
||||||
orange: '#FFA800',
|
|
||||||
green: '#00B150',
|
|
||||||
purple2: '#502EFF',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
colors: {
|
|
||||||
primary: '#0149FF',
|
|
||||||
secondary: '#537CFF',
|
|
||||||
background: '#090920',
|
|
||||||
success: '#00AC26',
|
|
||||||
help: '#FFC600',
|
|
||||||
error: '#FF0149',
|
|
||||||
surface: '#090920',
|
|
||||||
purple: '#A18EFF',
|
|
||||||
blue6: '#091c45',
|
|
||||||
blue5: '#2196f3',
|
|
||||||
blue4: '#0059D0',
|
|
||||||
blue2: '#003ACD',
|
|
||||||
yellow: '#FFC600',
|
|
||||||
yellow2: '#FFB701',
|
|
||||||
orange: '#FFA800',
|
|
||||||
warning: '#FF8A00',
|
|
||||||
// green: '#00B150',
|
|
||||||
green: '#00e366',
|
|
||||||
purple2: '#A18EFF',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaults: {
|
defaults: {
|
||||||
global: {
|
global: {
|
||||||
// ripple: false,
|
// ripple: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user