web/satellite: migrate webpack to vite

Migrated webpack to vite.
Replaced jest with vitest (because I couldn't resolve 'import.meta' issue)
Replaced legacy vue-svg-loader with vite-svg-loader
Replaced bip39 dep with bip39-english (it includes only english wordlist)
Replaced aws-sdk v2 with aws-sdk V3 (because v2 was throwing some weird error in console. Ref: https://stackoverflow.com/questions/75107933/aws-sdk-contributes-to-build-error-uncaught-typeerror-e-is-not-a-constructor)
Renamed VUE_APP_ENDPOINT_URL env variable to VITE_ENDPOINT_URL
Removed a ton of dependencies (like babel and jest-related stuff).

Tested in Chrome, Safari, Brave, Firefox and Opera browsers.

Additionally fixed logout errors from buckets and object browser routes.

TODO: try to remove util and stream-browserify dependencies and see if it works

Change-Id: I4562649a59eb0ba80c1a672d55c59fceb8c80b23
This commit is contained in:
Vitalii 2023-05-23 17:28:48 +03:00 committed by Vitalii Shpital
parent 4791a6422d
commit e6959004c9
44 changed files with 6528 additions and 24303 deletions

View File

@ -1 +1 @@
VUE_APP_ENDPOINT_URL=/api/v0/graphql
VITE_ENDPOINT_URL=/api/v0/graphql

View File

@ -18,11 +18,8 @@ module.exports = {
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaVersion: 2020,
vueFeatures: {
filter: true,
},
},
plugins: ['storj', 'eslint-plugin-import'],
plugins: ['eslint-plugin-import'],
rules: {
'linebreak-style': ['error', 'unix'],
@ -89,8 +86,6 @@ module.exports = {
'vue/no-undef-components': ['warn', { ignorePatterns: ['router-link', 'router-view'] }],
'storj/vue/require-annotation': 'warn',
'vue/no-v-html': ['error'],
},
settings: {

View File

@ -8,5 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,20 +0,0 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
process.env.TZ = 'UTC';
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript',
setupFiles: ['./jest.setup.ts'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.svg$': '<rootDir>/tests/unit/mock/svgTransform.js',
},
modulePathIgnorePatterns: ['<rootDir>/tests/unit/ignore'],
moduleFileExtensions: [
'js',
'ts',
'json',
'vue',
],
};

View File

@ -1,14 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { GlobalWithFetchMock } from 'jest-fetch-mock';
const customGlobal = (global as unknown) as
(GlobalWithFetchMock & { console: Record<any,unknown> });
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;
// Disallow warnings and errors from console.
customGlobal.console.warn = (message) => { throw new Error(message); };
customGlobal.console.error = (message) => { throw new Error(message); };

File diff suppressed because it is too large Load Diff

View File

@ -3,22 +3,25 @@
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint . --max-warnings 0 --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint . --max-warnings 0 --no-fix",
"build": "vue-cli-service build",
"preview": "vite preview",
"dev": "vite",
"build": "vite build",
"build-watch": "vite build --watch",
"lint": "eslint --ext .js,.ts,.vue src --fix && stylelint . --max-warnings 0 --fix",
"lint-ci": "eslint --ext .js,.ts,.vue src --no-fix && stylelint . --max-warnings 0 --no-fix",
"wasm": "chmod +x ./scripts/build-wasm.sh && ./scripts/build-wasm.sh",
"wasm-dev": "chmod +x ./scripts/build-wasm-dev.sh && ./scripts/build-wasm-dev.sh",
"dev": "vue-cli-service build --watch --no-module",
"test": "vue-cli-service test:unit"
"test": "vitest run"
},
"dependencies": {
"@apollo/client": "3.7.14",
"@aws-sdk/client-s3": "3.338.0",
"@aws-sdk/lib-storage": "3.338.0",
"@aws-sdk/s3-request-presigner": "3.338.0",
"@aws-sdk/signature-v4": "3.338.0",
"@hcaptcha/vue3-hcaptcha": "1.2.1",
"aws-sdk": "2.1377.0",
"bip39": "3.1.0",
"bip39-english": "2.5.0",
"chart.js": "4.2.1",
"core-js": "3.30.2",
"graphql": "15.3.0",
"pinia": "2.0.23",
"pretty-bytes": "5.6.0",
@ -32,48 +35,34 @@
},
"devDependencies": {
"@types/filesystem": "0.0.32",
"@types/jest": "27.5.2",
"@types/node": "16.18.14",
"@types/qrcode": "1.5.0",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@vue/cli-plugin-babel": "5.0.8",
"@vue/cli-plugin-eslint": "5.0.8",
"@vue/cli-plugin-typescript": "5.0.8",
"@vue/cli-plugin-unit-jest": "5.0.8",
"@vue/cli-service": "5.0.8",
"@vitejs/plugin-vue": "4.2.3",
"@vue/eslint-config-typescript": "11.0.3",
"@vue/test-utils": "2.3.2",
"@vue/vue3-jest": "27.0.0",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "27.5.1",
"compression-webpack-plugin": "9.2.0",
"eslint": "8.40.0",
"eslint-import-resolver-custom-alias": "1.3.0",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-storj": "0.0.2",
"eslint-plugin-vue": "9.12.0",
"jest": "27.5.1",
"jest-fetch-mock": "3.0.3",
"jsdom": "22.0.0",
"postcss-html": "1.5.0",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.62.1",
"sass-loader": "13.2.2",
"stylelint": "15.6.1",
"stylelint-config-standard": "33.0.0",
"stylelint-config-standard-scss": "9.0.0",
"stylelint-config-standard-vue": "1.0.0",
"stylelint-scss": "5.0.0",
"ts-jest": "27.1.5",
"typescript": "4.9.5",
"vue-eslint-parser": "9.2.1",
"vue-svg-loader": "0.17.0-beta.2",
"webpack-bundle-analyzer": "4.8.0"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
"vite": "4.3.9",
"vite-plugin-compression": "0.5.1",
"vite-plugin-require": "1.1.10",
"vite-svg-loader": "4.0.0",
"vitest": "0.31.1",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.0"
},
"browserslist": [
"defaults"

View File

@ -107,7 +107,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { generateMnemonic } from 'bip39';
import { generateMnemonic } from 'bip39-english';
import { useRoute, useRouter } from 'vue-router';
import { useNotify } from '@/utils/hooks';

View File

@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import { computed, Component } from 'vue';
import { computed } from 'vue';
import VTableCheckbox from '@/components/common/VTableCheckbox.vue';
import BucketGuide from '@/components/objects/BucketGuide.vue';
@ -84,7 +84,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits(['selectClicked']);
const icons = new Map<string, Component>([
const icons = new Map<string, string>([
['locked', TableLockedIcon],
['bucket', ColorBucketIcon],
['folder', ColorFolderIcon],

View File

@ -50,7 +50,7 @@
</template>
<script setup lang="ts">
import { computed, Component } from 'vue';
import { computed } from 'vue';
import WhitePlusIcon from '@/../static/images/common/plusWhite.svg';
import AddCircleIcon from '@/../static/images/common/addCircle.svg';
@ -106,7 +106,7 @@ const props = withDefaults(defineProps<{
onPress: () => {},
});
const icons = new Map<string, Component>([
const icons = new Map<string, string>([
['copy', CopyIcon],
['check', CheckIcon],
['download', DownloadIcon],
@ -120,7 +120,7 @@ const icons = new Map<string, Component>([
['add', WhitePlusIcon],
]);
const iconComponent = computed((): Component | undefined => icons.get(props.icon.toLowerCase()));
const iconComponent = computed((): string | undefined => icons.get(props.icon.toLowerCase()));
const containerClassName = computed((): string => {
if (props.isDisabled) return 'disabled';

View File

@ -12,7 +12,7 @@
</template>
<script setup lang="ts">
import { computed, Component } from 'vue';
import { computed } from 'vue';
import { ShareButtonConfig, ShareOptions } from '@/types/browser';
@ -29,7 +29,7 @@ import EmailIcon from '@/../static/images/objects/email.svg';
const props = defineProps<{ link: string; }>();
const images: Record<string, Component> = {
const images: Record<string, string> = {
[ShareOptions.Reddit]: RedditIcon,
[ShareOptions.Facebook]: FacebookIcon,
[ShareOptions.Twitter]: TwitterIcon,

View File

@ -8,7 +8,7 @@
</template>
<script setup lang="ts">
import { Component, computed } from 'vue';
import { computed, Component } from 'vue';
import { useAppStore } from '@/store/modules/appStore';

View File

@ -9,7 +9,7 @@
<img
v-if="previewAndMapFailed"
class="failed-preview"
src="/static/static/images/common/errorNotice.svg"
:src="ErrorNoticeIcon"
alt="failed preview"
>
<template v-else>
@ -126,6 +126,7 @@ import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import ErrorNoticeIcon from '@/../static/images/common/errorNotice.svg?url';
import PlaceholderImage from '@/../static/images/browser/placeholder.svg';
const appStore = useAppStore();

View File

@ -38,7 +38,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { generateMnemonic } from 'bip39';
import { generateMnemonic } from 'bip39-english';
import { useRoute, useRouter } from 'vue-router';
import { useNotify } from '@/utils/hooks';

View File

@ -12,10 +12,8 @@
</template>
<script setup lang="ts">
import { Component } from 'vue';
const props = defineProps<{
icon?: Component;
icon?: string;
title: string;
}>();
</script>

View File

@ -13,14 +13,12 @@
</template>
<script setup lang="ts">
import { Component } from 'vue';
import CloseIcon from '@/../static/images/notifications/closeSmall.svg';
const props = defineProps<{
wordingBold: string;
wording: string;
notificationIcon: Component;
notificationIcon: string;
warningNotification: boolean;
onCloseClick: () => void;
}>();

View File

@ -70,6 +70,8 @@ const edgeCredentials = computed((): EdgeCredentials => {
* Bucket from store found by router prop.
*/
const bucket = computed((): Bucket => {
if (!projectsStore.state.selectedProject.id) return new Bucket();
const data = bucketsStore.state.page.buckets.find(
(bucket: Bucket) => bucket.name === route.query.bucketName,
);

View File

@ -171,7 +171,7 @@ async function fetchBuckets(page = 1, limit: number): Promise<void> {
try {
await bucketsStore.getBuckets(page, projectsStore.state.selectedProject.id, limit);
} catch (error) {
await notify.error(`Unable to fetch buckets. ${error.message}`, AnalyticsErrorEventSource.BUCKET_TABLE);
notify.error(`Unable to fetch buckets. ${error.message}`, AnalyticsErrorEventSource.BUCKET_TABLE);
}
}
@ -180,14 +180,14 @@ async function fetchBuckets(page = 1, limit: number): Promise<void> {
*/
async function searchBuckets(searchQuery: string): Promise<void> {
bucketsStore.setBucketsSearch(searchQuery);
await analytics.eventTriggered(AnalyticsEvent.SEARCH_BUCKETS);
analytics.eventTriggered(AnalyticsEvent.SEARCH_BUCKETS);
searchLoading.value = true;
try {
await bucketsStore.getBuckets(1, projectsStore.state.selectedProject.id);
} catch (error) {
await notify.error(`Unable to fetch buckets: ${error.message}`, AnalyticsErrorEventSource.BUCKET_TABLE);
notify.error(`Unable to fetch buckets: ${error.message}`, AnalyticsErrorEventSource.BUCKET_TABLE);
}
searchLoading.value = false;
@ -219,7 +219,7 @@ async function openBucket(bucketName: string): Promise<void> {
await bucketsStore.setS3Client(projectsStore.state.selectedProject.id);
overallLoading.value = false;
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.BUCKET_TABLE);
notify.error(error.message, AnalyticsErrorEventSource.BUCKET_TABLE);
overallLoading.value = false;
return;
}

View File

@ -138,6 +138,8 @@ onMounted(async (): Promise<void> => {
});
watch(selectedProjectID, async () => {
if (!selectedProjectID.value) return;
isLoading.value = true;
bucketsStore.clear();

View File

@ -238,17 +238,18 @@ onBeforeMount(() => {
});
watch(passphrase, async () => {
const projectID = projectsStore.state.selectedProject.id;
if (!projectID) return;
if (!passphrase.value) {
await router.push(RouteConfig.Buckets.with(RouteConfig.BucketsManagement).path).catch(() => {return;});
return;
}
const projectID = projectsStore.state.selectedProject.id;
try {
await bucketsStore.setS3Client(projectID);
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
return;
}
@ -266,7 +267,7 @@ watch(passphrase, async () => {
obStore.getObjectCount(),
]);
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
}
});
</script>

View File

@ -17,12 +17,10 @@
</template>
<script setup lang="ts">
import { Component } from 'vue';
import VLoader from '@/components/common/VLoader.vue';
const props = withDefaults(defineProps<{
icon: Component,
icon: string,
isDataFetching: boolean,
title: string,
subtitle: string,

View File

@ -36,12 +36,12 @@
</template>
<script setup lang="ts">
import { computed, Component } from 'vue';
import { computed } from 'vue';
import VLoader from '@/components/common/VLoader.vue';
const props = withDefaults(defineProps<{
icon: Component
icon: string
title: string
color: string
usedValue: number

View File

@ -7,7 +7,6 @@
<h1 class="project-dashboard__heading__title" aria-roledescription="title">{{ selectedProject.name }}</h1>
<project-ownership-tag :is-owner="selectedProject.ownerId === user.id" />
</div>
<p class="project-dashboard__message">
Expect a delay of a few hours between network activity and the latest dashboard stats.
</p>

View File

@ -42,7 +42,10 @@ export const useAccessGrantsStore = defineStore('accessGrants', () => {
const configStore = useConfigStore();
async function startWorker(): Promise<void> {
const worker = new Worker(new URL('@/utils/accessGrant.worker.js', import.meta.url), { type: 'module' });
// TODO(vitalii): create an issue here https://github.com/vitejs/vite
// about worker chunk being auto removed after rebuild in watch mode if using new URL constructor.
// const worker = new Worker(new URL('@/utils/accessGrant.worker.js', import.meta.url));
const worker = new Worker('/static/src/utils/accessGrant.worker.js');
worker.postMessage({ 'type': 'Setup' });
const event: MessageEvent = await new Promise(resolve => worker.onmessage = resolve);

View File

@ -169,6 +169,7 @@ export const useAppStore = defineStore('app', () => {
state.selectedPricingPlan = null;
state.hasShownPricingPlan = false;
state.activeDropdown = '';
state.isUploadingModal = false;
state.error.visible = false;
}

View File

@ -3,7 +3,14 @@
import { reactive } from 'vue';
import { defineStore } from 'pinia';
import S3 from 'aws-sdk/clients/s3';
import {
S3Client,
S3ClientConfig,
CreateBucketCommand,
DeleteBucketCommand,
ListObjectsV2Command,
} from '@aws-sdk/client-s3';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets';
import { BucketsApiGql } from '@/api/buckets';
@ -23,20 +30,17 @@ export class BucketsState {
public edgeCredentials: EdgeCredentials = new EdgeCredentials();
public edgeCredentialsForDelete: EdgeCredentials = new EdgeCredentials();
public edgeCredentialsForCreate: EdgeCredentials = new EdgeCredentials();
public s3Client: S3 = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
public s3Client: S3Client = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
public s3ClientForDelete: S3 = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
public s3ClientForDelete: S3Client = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
public s3ClientForCreate: S3 = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
public s3ClientForCreate: S3Client = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
public apiKey = '';
public passphrase = '';
@ -81,31 +85,35 @@ export const useBucketsStore = defineStore('buckets', () => {
function setEdgeCredentialsForDelete(credentials: EdgeCredentials): void {
state.edgeCredentialsForDelete = credentials;
const s3Config = {
accessKeyId: state.edgeCredentialsForDelete.accessKeyId,
secretAccessKey: state.edgeCredentialsForDelete.secretKey,
const s3Config: S3ClientConfig = {
credentials: {
accessKeyId: state.edgeCredentialsForDelete.accessKeyId,
secretAccessKey: state.edgeCredentialsForDelete.secretKey,
},
endpoint: state.edgeCredentialsForDelete.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
forcePathStyle: true,
signerConstructor: SignatureV4,
region: 'us-east-1',
};
state.s3ClientForDelete = new S3(s3Config);
state.s3ClientForDelete = new S3Client(s3Config);
}
function setEdgeCredentialsForCreate(credentials: EdgeCredentials): void {
state.edgeCredentialsForCreate = credentials;
const s3Config = {
accessKeyId: state.edgeCredentialsForCreate.accessKeyId,
secretAccessKey: state.edgeCredentialsForCreate.secretKey,
const s3Config: S3ClientConfig = {
credentials: {
accessKeyId: state.edgeCredentialsForCreate.accessKeyId,
secretAccessKey: state.edgeCredentialsForCreate.secretKey,
},
endpoint: state.edgeCredentialsForCreate.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
forcePathStyle: true,
signerConstructor: SignatureV4,
region: 'us-east-1',
};
state.s3ClientForCreate = new S3(s3Config);
state.s3ClientForCreate = new S3Client(s3Config);
}
async function setS3Client(projectID: string): Promise<void> {
@ -171,16 +179,18 @@ export const useBucketsStore = defineStore('buckets', () => {
const accessGrant = accessGrantEvent.data.value;
state.edgeCredentials = await agStore.getEdgeCredentials(accessGrant);
const s3Config = {
accessKeyId: state.edgeCredentials.accessKeyId,
secretAccessKey: state.edgeCredentials.secretKey,
const s3Config: S3ClientConfig = {
credentials: {
accessKeyId: state.edgeCredentials.accessKeyId,
secretAccessKey: state.edgeCredentials.secretKey,
},
endpoint: state.edgeCredentials.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
forcePathStyle: true,
signerConstructor: SignatureV4,
region: 'us-east-1',
};
state.s3Client = new S3(s3Config);
state.s3Client = new S3Client(s3Config);
}
function setPassphrase(passphrase: string): void {
@ -192,27 +202,27 @@ export const useBucketsStore = defineStore('buckets', () => {
}
async function createBucket(name: string): Promise<void> {
await state.s3Client.createBucket({
await state.s3Client.send(new CreateBucketCommand({
Bucket: name,
}).promise();
}));
}
async function createBucketWithNoPassphrase(name: string): Promise<void> {
await state.s3ClientForCreate.createBucket({
await state.s3ClientForCreate.send(new CreateBucketCommand({
Bucket: name,
}).promise();
}));
}
async function deleteBucket(name: string): Promise<void> {
await state.s3ClientForDelete.deleteBucket({
await state.s3ClientForDelete.send(new DeleteBucketCommand({
Bucket: name,
}).promise();
}));
}
async function getObjectsCount(name: string): Promise<number> {
const response = await state.s3Client.listObjectsV2({
const response = await state.s3Client.send(new ListObjectsV2Command({
Bucket: name,
}).promise();
}));
return response.KeyCount === undefined ? 0 : response.KeyCount;
}
@ -224,20 +234,17 @@ export const useBucketsStore = defineStore('buckets', () => {
state.edgeCredentials = new EdgeCredentials();
state.edgeCredentialsForDelete = new EdgeCredentials();
state.edgeCredentialsForCreate = new EdgeCredentials();
state.s3Client = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
state.s3Client = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
state.s3ClientForDelete = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
state.s3ClientForDelete = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
state.s3ClientForCreate = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
state.s3ClientForCreate = new S3Client({
forcePathStyle: true,
signerConstructor: SignatureV4,
});
state.fileComponentBucketName = '';
state.leaveRoute = '';

View File

@ -3,7 +3,20 @@
import { computed, reactive } from 'vue';
import { defineStore } from 'pinia';
import S3, { CommonPrefix } from 'aws-sdk/clients/s3';
import {
S3Client,
CommonPrefix,
S3ClientConfig,
ListObjectsCommand,
ListObjectsV2Command,
DeleteObjectCommand,
PutObjectCommand,
_Object,
GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
@ -46,7 +59,7 @@ export type UploadingBrowserObject = BrowserObject & {
}
export class FilesState {
s3: S3 | null = null;
s3: S3Client | null = null;
accessKey: null | string = null;
path = '';
bucket = '';
@ -70,7 +83,7 @@ export class FilesState {
}
type InitializedFilesState = FilesState & {
s3: S3;
s3: S3Client;
};
function assertIsInitialized(
@ -157,17 +170,18 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
fetchSharedLink: (arg0: string) => Promisable<string>;
fetchPreviewAndMapUrl: (arg0: string) => Promisable<string>;
}): void {
const s3Config = {
accessKeyId: accessKey,
secretAccessKey: secretKey,
const s3Config: S3ClientConfig = {
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretKey,
},
endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
connectTimeout: 0,
httpOptions: { timeout: 0 },
forcePathStyle: true,
signerConstructor: SignatureV4,
region: 'us-east-1',
};
state.s3 = new S3(s3Config);
state.s3 = new S3Client(s3Config);
state.accessKey = accessKey;
state.bucket = bucket;
state.browserRoot = browserRoot;
@ -187,18 +201,19 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
secretKey: string;
endpoint: string;
}): void {
const s3Config = {
accessKeyId: accessKey,
secretAccessKey: secretKey,
const s3Config: S3ClientConfig = {
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretKey,
},
endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
connectTimeout: 0,
httpOptions: { timeout: 0 },
forcePathStyle: true,
signerConstructor: SignatureV4,
region: 'us-east-1',
};
state.files = [];
state.s3 = new S3(s3Config);
state.s3 = new S3Client(s3Config);
state.accessKey = accessKey;
}
@ -214,24 +229,20 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
assertIsInitialized(state);
const response = await state.s3
.listObjects({
Bucket: state.bucket,
Delimiter: '/',
Prefix: path,
})
.promise();
const response = await state.s3.send(new ListObjectsCommand({
Bucket: state.bucket,
Delimiter: '/',
Prefix: path,
}));
const { Contents, CommonPrefixes } = response;
let { Contents, CommonPrefixes } = response;
if (Contents === undefined) {
throw new Error('Bad S3 listObjects() response: "Contents" undefined');
Contents = [];
}
if (CommonPrefixes === undefined) {
throw new Error(
'Bad S3 listObjects() response: "CommonPrefixes" undefined',
);
CommonPrefixes = [];
}
Contents.sort((a, b) => {
@ -302,11 +313,9 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
async function getObjectCount(): Promise<void> {
assertIsInitialized(state);
const responseV2 = await state.s3
.listObjectsV2({
Bucket: state.bucket,
})
.promise();
const responseV2 = await state.s3.send(new ListObjectsV2Command({
Bucket: state.bucket,
}));
state.objectsCount = responseV2.KeyCount === undefined ? 0 : responseV2.KeyCount;
}
@ -375,7 +384,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
const fileNames = state.files.map((file) => file.Key);
function getUniqueFileName(fileName) {
function getUniqueFileName(fileName: string): string {
for (let count = 1; fileNames.includes(fileName); count++) {
if (count > 1) {
fileName = fileName.replace(/\((\d+)\)(.*)/, `(${count})$2`);
@ -415,10 +424,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
appStore.setLargeUploadWarningNotification(true);
}
const upload = state.s3.upload(
{ ...params },
{ partSize: 64 * 1024 * 1024 },
);
const upload = new Upload({
client: state.s3,
partSize: 64 * 1024 * 1024,
params,
});
upload.on('httpUploadProgress', async (progress) => {
const file = state.uploading.find(file => file.Key === params.Key);
@ -426,7 +436,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
throw new Error(`No file found with key ${JSON.stringify(params.Key)}`);
}
file.progress = Math.round((progress.loaded / progress.total) * 100);
let p = 0;
if (progress.loaded && progress.total) {
p = Math.round((progress.loaded / progress.total) * 100);
}
file.progress = p;
});
if (config.state.config.newUploadModalEnabled) {
@ -473,7 +487,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
}
try {
await upload.promise();
await upload.done();
state.uploading[index].status = UploadingStatus.Finished;
} catch (error) {
handleUploadError(error.message, index);
@ -509,10 +523,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Body: item.Body,
};
const upload = state.s3.upload(
{ ...params },
{ partSize: 64 * 1024 * 1024 },
);
const upload = new Upload({
client: state.s3,
partSize: 64 * 1024 * 1024,
params,
});
upload.on('httpUploadProgress', async (progress) => {
const file = state.uploading.find(file => file.Key === params.Key);
@ -520,7 +535,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
throw new Error(`No file found with key ${JSON.stringify(params.Key)}`);
}
file.progress = Math.round((progress.loaded / progress.total) * 100);
let p = 0;
if (progress.loaded && progress.total) {
p = Math.round((progress.loaded / progress.total) * 100);
}
file.progress = p;
});
state.uploading[index] = {
@ -533,7 +552,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
};
try {
await upload.promise();
await upload.done();
state.uploading[index].status = UploadingStatus.Finished;
} catch (error) {
handleUploadError(error.message, index);
@ -563,29 +582,26 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
async function createFolder(name): Promise<void> {
assertIsInitialized(state);
await state.s3
.putObject({
Bucket: state.bucket,
Key: state.path + name + '/.file_placeholder',
})
.promise();
await state.s3.send(new PutObjectCommand({
Bucket: state.bucket,
Key: state.path + name + '/.file_placeholder',
Body: '',
}));
list();
}
async function deleteObject(path: string, file?: S3.Object | BrowserObject, isFolder = false): Promise<void> {
async function deleteObject(path: string, file?: _Object | BrowserObject, isFolder = false): Promise<void> {
if (!file) {
return;
}
assertIsInitialized(state);
await state.s3
.deleteObject({
Bucket: state.bucket,
Key: path + file.Key,
})
.promise();
await state.s3.send(new DeleteObjectCommand({
Bucket: state.bucket,
Key: path + file.Key,
}));
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
@ -604,31 +620,23 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
async function recurse(filePath) {
assertIsInitialized(state);
const { Contents, CommonPrefixes } = await state.s3
.listObjects({
Bucket: state.bucket,
Delimiter: '/',
Prefix: filePath,
})
.promise();
let { Contents, CommonPrefixes } = await state.s3.send(new ListObjectsCommand({
Bucket: state.bucket,
Delimiter: '/',
Prefix: filePath,
}));
if (Contents === undefined) {
throw new Error(
'Bad S3 listObjects() response: "Contents" undefined',
);
Contents = [];
}
if (CommonPrefixes === undefined) {
throw new Error(
'Bad S3 listObjects() response: "CommonPrefixes" undefined',
);
CommonPrefixes = [];
}
async function thread() {
if (Contents === undefined) {
throw new Error(
'Bad S3 listObjects() response: "Contents" undefined',
);
Contents = [];
}
while (Contents.length) {
@ -679,10 +687,10 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
function download(file): void {
assertIsInitialized(state);
const url = state.s3.getSignedUrl('getObject', {
const url = getSignedUrl(state.s3, new GetObjectCommand({
Bucket: state.bucket,
Key: state.path + file.Key,
});
}));
const downloadURL = function(data, fileName) {
const a = document.createElement('a');
a.href = data;

View File

@ -1,8 +1,6 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { Component } from 'vue';
import { getId } from '@/utils/idGenerator';
import SuccessIcon from '@/../static/images/notifications/success.svg';
@ -32,7 +30,7 @@ export class DelayedNotification {
public readonly type: string;
public readonly message: string;
public readonly style: { backgroundColor: string };
public readonly icon: Component;
public readonly icon: string;
constructor(callback: () => void, type: string, message: string) {
this.callback = callback;

View File

@ -1,20 +1,8 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { Component } from 'vue';
import EmailIcon from '../../static/images/objects/email.svg';
/**
* Exposes all properties and methods present and available in the file/browser objects in Browser.
*/
export interface BrowserFile extends File {
Key: string;
LastModified: Date;
Size: number;
type: string;
}
export enum ShareOptions {
Reddit = 'Reddit',
Facebook = 'Facebook',
@ -31,6 +19,6 @@ export class ShareButtonConfig {
public label: ShareOptions = ShareOptions.Email,
public color: string = 'white',
public link: string = '',
public image: Component = EmailIcon,
public image: string = EmailIcon,
) {}
}

View File

@ -1,8 +1,6 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { Component } from 'vue';
import CreateNewAccessIcon from '@/../static/images/accessGrants/newCreateFlow/createNewAccess.svg';
import ChoosePermissionIcon from '@/../static/images/accessGrants/newCreateFlow/choosePermission.svg';
import AccessEncryptionIcon from '@/../static/images/accessGrants/newCreateFlow/accessEncryption.svg';
@ -20,7 +18,7 @@ import EndDateIcon from '@/../static/images/accessGrants/newCreateFlow/endDateIc
import EncryptionPassphraseIcon from '@/../static/images/accessGrants/newCreateFlow/encryptionPassphraseIcon.svg';
export interface IconAndTitle {
icon: Component;
icon: string;
title: string;
}

View File

@ -1,14 +1,12 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { Component } from 'vue';
export class NavigationLink {
private readonly _path: string;
private readonly _name: string;
private readonly _icon: Component | undefined;
private readonly _icon: string | undefined;
public constructor(path: string, name: string, icon?: Component) {
public constructor(path: string, name: string, icon?: string) {
this._path = path;
this._name = name;
this._icon = icon;
@ -22,7 +20,7 @@ export class NavigationLink {
return this._name;
}
public get icon(): Component | undefined {
public get icon(): string | undefined {
return this._icon;
}
@ -30,7 +28,7 @@ export class NavigationLink {
return this._path[0] !== '/';
}
public withIcon(icon: Component): NavigationLink {
public withIcon(icon: string): NavigationLink {
return new NavigationLink(this._path, this._name, icon);
}

View File

@ -1,8 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
declare module '*.svg' {
import { Component } from 'vue';
const content: Component;
export default content;
}

View File

@ -1,9 +1,11 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
// eslint-disable-next-line
const component: DefineComponent<any, any, any>;
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -15,7 +15,7 @@ self.onmessage = async function (event) {
switch (data.type) {
case 'Setup':
try {
const go = new global.Go();
const go = new self.Go();
const response = await fetch('/static/static/wasm/access.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);

View File

@ -13,7 +13,7 @@ import { useNotificationsStore } from '@/store/modules/notificationsStore';
* Satellite url.
*/
const satelliteUrl = new HttpLink({
uri: process.env.VUE_APP_ENDPOINT_URL,
uri: import.meta.env.VITE_ENDPOINT_URL,
});
/**

View File

@ -96,7 +96,7 @@ onMounted(() => {
padding: 52px 24px;
box-sizing: border-box;
display: flex;
background: url('~@/../static/images/errors/dotWorld.png') no-repeat center 178px;
background: url('../../static/images/errors/dotWorld.png') no-repeat center 178px;
flex-direction: column;
justify-content: center;
overflow-y: auto;

View File

@ -281,11 +281,6 @@ import { computed, onBeforeMount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import BottomArrowIcon from '../../../static/images/common/lightBottomArrow.svg';
import SelectedCheckIcon from '../../../static/images/common/selectedCheck.svg';
import LogoIcon from '../../../static/images/logo.svg';
import LogoWithPartnerIcon from '../../../static/images/partnerStorjLogo.svg';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { MultiCaptchaConfig, PartneredSatellite } from '@/types/config';
@ -299,6 +294,10 @@ import VButton from '@/components/common/VButton.vue';
import VInput from '@/components/common/VInput.vue';
import AddCouponCodeInput from '@/components/common/AddCouponCodeInput.vue';
import LogoWithPartnerIcon from '@/../static/images/partnerStorjLogo.svg';
import LogoIcon from '@/../static/images/logo.svg';
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
import RegisterGlobe from '@/../static/images/register/RegisterGlobe.svg';
import InfoIcon from '@/../static/images/register/info.svg';

View File

@ -1,4 +1,3 @@
{
"default": {
"title": "Welcome to the distributed cloud.",

View File

@ -1,8 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
module.exports = {
process () {
return `module.exports = { render: function(){ return this._c("svg") } }`;
},
};

View File

@ -1,6 +1,7 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { vi } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { ProjectMembersApiGql } from '@/api/projectMembers';
@ -23,7 +24,7 @@ const projectMember2 = new ProjectMember('testFullName2', 'testShortName2', 'tes
describe('actions', () => {
beforeEach(() => {
setActivePinia(createPinia());
jest.resetAllMocks();
vi.resetAllMocks();
});
it('fetch project members', async function () {
@ -34,7 +35,7 @@ describe('actions', () => {
testProjectMembersPage.totalCount = 1;
testProjectMembersPage.pageCount = 1;
jest.spyOn(ProjectMembersApiGql.prototype, 'get')
vi.spyOn(ProjectMembersApiGql.prototype, 'get')
.mockImplementation(() => Promise.resolve(testProjectMembersPage));
await store.getProjectMembers(FIRST_PAGE, selectedProject.id);
@ -95,7 +96,7 @@ describe('actions', () => {
expect(store.state.page.projectMembers[0].isSelected).toBe(true);
expect(store.state.selectedProjectMembersEmails.length).toBe(1);
jest.spyOn(ProjectMembersApiGql.prototype, 'get')
vi.spyOn(ProjectMembersApiGql.prototype, 'get')
.mockImplementation(() => Promise.resolve(testProjectMembersPage));
await store.getProjectMembers(FIRST_PAGE, selectedProject.id);
@ -130,7 +131,7 @@ describe('actions', () => {
it('add project members', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'add').mockReturnValue(Promise.resolve());
vi.spyOn(ProjectMembersApiGql.prototype, 'add').mockReturnValue(Promise.resolve());
try {
await store.addProjectMembers([projectMember1.user.email], selectedProject.id);
@ -143,7 +144,7 @@ describe('actions', () => {
it('add project member throws error when api call fails', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'add').mockImplementation(() => {
vi.spyOn(ProjectMembersApiGql.prototype, 'add').mockImplementation(() => {
throw TEST_ERROR;
});
@ -164,7 +165,7 @@ describe('actions', () => {
it('delete project members', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'delete').mockReturnValue(Promise.resolve());
vi.spyOn(ProjectMembersApiGql.prototype, 'delete').mockReturnValue(Promise.resolve());
try {
await store.deleteProjectMembers(selectedProject.id);
@ -177,7 +178,7 @@ describe('actions', () => {
it('delete project member throws error when api call fails', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'delete').mockImplementation(() => {
vi.spyOn(ProjectMembersApiGql.prototype, 'delete').mockImplementation(() => {
throw TEST_ERROR;
});
@ -198,7 +199,7 @@ describe('actions', () => {
it('fetch project members', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'get').mockReturnValue(
vi.spyOn(ProjectMembersApiGql.prototype, 'get').mockReturnValue(
Promise.resolve(new ProjectMembersPage(
[projectMember1],
'',
@ -223,7 +224,7 @@ describe('actions', () => {
it('fetch project members throws error when api call fails', async function () {
const store = useProjectMembersStore();
jest.spyOn(ProjectMembersApiGql.prototype, 'get').mockImplementation(() => {
vi.spyOn(ProjectMembersApiGql.prototype, 'get').mockImplementation(() => {
throw TEST_ERROR;
});

View File

@ -1,6 +1,7 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { vi } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { ProjectsApiGql } from '@/api/projects';
@ -31,7 +32,7 @@ const projects = [
describe('actions', () => {
beforeEach(() => {
setActivePinia(createPinia());
jest.resetAllMocks();
vi.resetAllMocks();
});
it('select project', () => {
@ -47,7 +48,7 @@ describe('actions', () => {
it('success fetch projects', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'get').mockReturnValue(
vi.spyOn(ProjectsApiGql.prototype, 'get').mockReturnValue(
Promise.resolve(projects),
);
@ -59,7 +60,7 @@ describe('actions', () => {
it('fetch throws an error when api call fails', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'get').mockImplementation(() => { throw new Error(); });
vi.spyOn(ProjectsApiGql.prototype, 'get').mockImplementation(() => { throw new Error(); });
try {
await store.getProjects();
@ -72,7 +73,7 @@ describe('actions', () => {
it('success create project', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'create').mockReturnValue(
vi.spyOn(ProjectsApiGql.prototype, 'create').mockReturnValue(
Promise.resolve(project),
);
@ -85,7 +86,7 @@ describe('actions', () => {
it('create throws an error when create api call fails', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'create').mockImplementation(() => { throw new Error(); });
vi.spyOn(ProjectsApiGql.prototype, 'create').mockImplementation(() => { throw new Error(); });
try {
await store.createProject(new ProjectFields());
@ -99,7 +100,7 @@ describe('actions', () => {
it('success update project name', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'update').mockReturnValue(
vi.spyOn(ProjectsApiGql.prototype, 'update').mockReturnValue(
Promise.resolve(),
);
@ -115,7 +116,7 @@ describe('actions', () => {
it('success update project description', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'update').mockReturnValue(
vi.spyOn(ProjectsApiGql.prototype, 'update').mockReturnValue(
Promise.resolve(),
);
@ -131,7 +132,7 @@ describe('actions', () => {
it('success get project limits', async () => {
const store = useProjectsStore();
jest.spyOn(ProjectsApiGql.prototype, 'getLimits').mockReturnValue(
vi.spyOn(ProjectsApiGql.prototype, 'getLimits').mockReturnValue(
Promise.resolve(limits),
);

View File

@ -0,0 +1,77 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { resolve } from 'path';
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import vue from '@vitejs/plugin-vue';
import viteCompression from 'vite-plugin-compression';
import vitePluginRequire from 'vite-plugin-require';
import svgLoader from 'vite-svg-loader';
const productionBrotliExtensions = ['js', 'css', 'ttf', 'woff', 'woff2'];
const plugins = [
vue(),
viteCompression({
algorithm: 'brotliCompress',
threshold: 1024,
ext: '.br',
filter: new RegExp('\\.(' + productionBrotliExtensions.join('|') + ')$'),
}),
svgLoader({
svgoConfig: {
plugins: [{ name: 'removeViewBox', fn: () => {} }],
},
}),
vitePluginRequire(),
];
if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
plugins.push(visualizer({
template: 'treemap', // or sunburst
open: true,
brotliSize: true,
filename: 'analyse.html', // will be saved in project's root
}));
}
export default defineConfig(({ mode }) => {
return {
base: '/static/dist',
plugins,
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'stream': 'stream-browserify',
'util': 'util/',
},
extensions: ['.js', '.ts', '.svg', '.vue'],
},
build: {
outDir: resolve(__dirname, 'dist'),
emptyOutDir: true,
rollupOptions: {
output: {
experimentalMinChunkSize: 50*1024,
},
},
},
define: {
'process.env': {},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: [
'./vitest.setup.ts',
],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/tests/unit/ignore/**',
],
},
};
});

View File

@ -0,0 +1,9 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import createFetchMock from 'vitest-fetch-mock';
import { vi } from 'vitest';
const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();

View File

@ -1,85 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
const path = require('path');
const webpack = require('webpack');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionBrotliExtensions = ['js', 'css', 'ttf', 'woff', 'woff2'];
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const plugins = [
new CompressionWebpackPlugin({
algorithm: 'brotliCompress',
filename: '[path][name].br',
test: new RegExp('\\.(' + productionBrotliExtensions.join('|') + ')$'),
threshold: 1024,
minRatio: 0.8,
}),
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 50*1024,
}),
new webpack.IgnorePlugin({
contextRegExp: /bip39[\\/]src$/,
resourceRegExp: /^\.\/wordlists\/(?!english)/,
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
];
if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
plugins.push(new BundleAnalyzerPlugin());
}
module.exports = {
publicPath: '/static/dist',
productionSourceMap: false,
parallel: true,
lintOnSave: process.env.NODE_ENV !== 'production', // disables eslint for builds
configureWebpack: {
plugins,
resolve: {
fallback: {
'util': require.resolve('util/'),
'stream': require.resolve('stream-browserify'),
'buffer': require.resolve('buffer'),
},
},
},
chainWebpack: config => {
// Avoid breaking browser UI cache.
config.output.chunkFilename(`js/vendors_[name]_[chunkhash].js`);
config.output.filename(`js/app_[name]_[chunkhash].js`);
config.resolve.alias
.set('@', path.resolve('src'));
// Disable node_modules/.cache directory usage due to permissions.
// This is enabled by default in https://cli.vuejs.org/core-plugins/babel.html#caching.
config.module.rule('js').use('babel-loader')
.tap(options => Object.assign(options, { cacheDirectory: false }));
config
.plugin('html')
.tap(args => {
args[0].template = './index.html';
return args;
});
const svgRule = config.module.rule('svg');
svgRule.uses.clear();
svgRule.type(); // Disable webpack 5 asset management.
svgRule
.use('vue-loader')
.loader('vue-loader')
.end()
.use('vue-svg-loader')
.loader('vue-svg-loader')
.options({
svgo: {
plugins: [{ removeViewBox: false }],
},
});
},
};