Migrate from vue-cli / webpack to vite

This commit is contained in:
Linus Groh 2024-02-27 22:01:37 +00:00
parent 06faa73b70
commit 91d99cd8da
25 changed files with 1582 additions and 10975 deletions

View File

@ -1,42 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"max-len": [
"error",
{
ignoreUrls: true,
},
],
"prettier/prettier": [
"error",
{
trailingComma: "es5",
printWidth: 80,
htmlWhitespaceSensitivity: "ignore",
},
],
"vue/multi-word-component-names": [
"error",
{
ignores: ["Map"],
},
],
},
parserOptions: {
parser: "@babel/eslint-parser",
},
overrides: [
{
files: ["**/__tests__/*.{j,t}s?(x)"],
env: {
jest: true,
},
},
],
};

View File

@ -98,7 +98,7 @@ See [`docs/config.md`](docs/config.md) for all available options.
## Development
- Run `npm install` to install dependencies
- Run `npm run serve` to compile for development and start the hot-reload server
- Run `npm run dev` to compile for development and start the hot-reload server
- Run `npm run lint:js` to lint JavaScript/Vue files
- Run `npm run lint:md` to lint Markdown files
- Run `npm run lint:scss` to lint SCSS files

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

47
eslint.config.js Normal file
View File

@ -0,0 +1,47 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import eslintPluginVue from "eslint-plugin-vue";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import vueParser from "vue-eslint-parser";
import { FlatCompat } from "@eslint/eslintrc";
const eslintrc = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
});
export default [
...eslintrc.extends("plugin:vue/essential"),
eslintPluginPrettierRecommended,
{
languageOptions: {
parser: vueParser,
},
plugins: {
vue: eslintPluginVue,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"max-len": [
"error",
{
ignoreUrls: true,
},
],
"prettier/prettier": [
"error",
{
trailingComma: "es5",
printWidth: 80,
htmlWhitespaceSensitivity: "ignore",
},
],
"vue/multi-word-component-names": [
"error",
{
ignores: ["Map"],
},
],
},
},
];

View File

@ -4,8 +4,8 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="manifest" crossorigin="use-credentials" href="<%= BASE_URL %>manifest.json">
<link rel="icon" href="/favicon.ico">
<link rel="manifest" crossorigin="use-credentials" href="/manifest.json">
<title>OwnTracks Frontend</title>
</head>
<body>
@ -13,7 +13,7 @@
<strong>We're sorry but OwnTracks doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script src="<%= BASE_URL %>config/config.js"></script>
<!-- built files will be auto injected -->
<script src="/config/config.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -1,24 +0,0 @@
module.exports = {
testEnvironment: "jsdom",
moduleFileExtensions: ["js", "jsx", "json", "vue"],
transform: {
"^.+\\.vue$": "@vue/vue2-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
"jest-transform-stub",
"^.+\\.jsx?$": "babel-jest",
},
transformIgnorePatterns: ["/node_modules/"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
snapshotSerializers: ["jest-serializer-vue"],
testMatch: [
"**/tests/**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)",
],
testURL: "http://localhost/",
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
setupFiles: ["<rootDir>/tests/setup.js"],
};

11973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,31 @@
{
"name": "owntracks-frontend",
"version": "2.12.0",
"license": "MIT",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"
},
"repository": {
"type": "git",
"url": "https://github.com/owntracks/frontend.git"
},
"type": "module",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"cors-proxy": "node scripts/corsProxy.js",
"format:js": "vue-cli-service lint",
"format:js": "eslint --fix 'src/**/*.{js,vue}'",
"format:md": "prettier --write '{*.md,docs/**/*.md,src/**/*.md}'",
"format:scss": "prettier --write 'src/**/*.scss'",
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
"lint:js": "vue-cli-service lint --no-fix",
"lint:js": "eslint 'src/**/*.{js,vue}'",
"lint:md": "prettier --check '{*.md,docs/**/*.md,src/**/*.md}'",
"lint:scss": "prettier --check 'src/**/*.scss'",
"test": "vue-cli-service test:unit"
"test": "vitest run"
},
"dependencies": {
"clipboard-copy": "^4.0.1",
"core-js": "^3.36.0",
"deepmerge": "^4.3.1",
"leaflet": "^1.9.4",
"leaflet.heat": "^0.2.0",
@ -37,34 +42,21 @@
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.10",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-unit-jest": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/test-utils": "^1.3.6",
"@vue/vue2-jest": "^27.0.0",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^27.5.1",
"@eslint/eslintrc": "^3.0.2",
"@vitejs/plugin-vue2": "^2.3.1",
"cors-anywhere": "^0.4.4",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0",
"jest": "^27.5.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"moment-locales-webpack-plugin": "^1.2.0",
"prettier": "^3.2.5",
"sass": "^1.71.1",
"sass-loader": "^14.1.1",
"vue-cli-plugin-i18n": "^2.3.2",
"vue-template-compiler": "^2.7.16"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/owntracks/frontend.git"
"vite": "^5.1.4",
"vite-plugin-package-version": "^1.1.0",
"vitest": "^1.3.1",
"vitest-fetch-mock": "^0.2.2",
"vue-eslint-parser": "^9.4.2"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

View File

@ -1,4 +1,4 @@
const corsProxy = require("cors-anywhere");
import { createServer } from "cors-anywhere";
const host = process.env.OT_PROXY_HOST || "0.0.0.0";
const port = process.env.OT_PROXY_PORT || 8888;
@ -20,6 +20,6 @@ if (username !== null && password !== null) {
};
}
corsProxy.createServer(options).listen(port, host, () => {
createServer(options).listen(port, host, () => {
console.log(`Running CORS Anywhere on http://${host}:${port}`);
});

View File

@ -10,19 +10,15 @@
</div>
</template>
<style lang="scss">
@import "styles/main";
</style>
<script>
import { mapActions } from "vuex";
import * as types from "@/store/mutation-types";
import { log } from "@/logging";
import AppHeader from "@/components/AppHeader";
import DownloadModal from "@/components/modals/DownloadModal";
import InformationModal from "@/components/modals/InformationModal";
import LoadingModal from "@/components/modals/LoadingModal";
import AppHeader from "@/components/AppHeader.vue";
import DownloadModal from "@/components/modals/DownloadModal.vue";
import InformationModal from "@/components/modals/InformationModal.vue";
import LoadingModal from "@/components/modals/LoadingModal.vue";
export default {
components: { AppHeader, DownloadModal, InformationModal, LoadingModal },
@ -95,3 +91,7 @@ export default {
},
};
</script>
<style lang="scss">
@import "styles/main";
</style>

View File

@ -93,7 +93,7 @@
<option :value="null">
{{ $t("Show all") }}
</option>
<option v-for="user in users" :value="user" :key="user">
<option v-for="user in users" :key="user" :value="user">
{{ user }}
</option>
</select>
@ -110,8 +110,8 @@
</option>
<option
v-for="device in devices[selectedUser]"
:value="device"
:key="`${selectedUser}-${device}`"
:value="device"
>
{{ device }}
</option>
@ -161,18 +161,6 @@
</header>
</template>
<style lang="scss" scoped>
.distance-travelled {
text-align: right;
line-height: 1.2;
.feather {
margin-top: 3px;
margin-right: 0 !important;
}
}
</style>
<script>
import moment from "moment";
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
@ -191,7 +179,7 @@ import {
import VueCtkDateTimePicker from "vue-ctk-date-time-picker";
import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
import DropdownButton from "@/components/DropdownButton";
import DropdownButton from "@/components/DropdownButton.vue";
import { DATE_TIME_FORMAT } from "@/constants";
import * as types from "@/store/mutation-types";
import { humanReadableDistance } from "@/util";
@ -292,3 +280,15 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.distance-travelled {
text-align: right;
line-height: 1.2;
.feather {
margin-top: 3px;
margin-right: 0 !important;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="dropdown" v-focus-outside="hide" v-click-outside="hide">
<div v-focus-outside="hide" v-click-outside="hide" class="dropdown">
<button class="dropdown-button button" :title="title" @click="toggle">
{{ label }}
</button>

View File

@ -54,30 +54,6 @@
</LPopup>
</template>
<style lang="scss" scoped>
.device {
display: inline-block;
position: relative;
top: -5px;
color: var(--color-primary);
font-weight: bold;
}
.wrapper {
display: flex;
margin-top: 10px;
img {
align-self: start;
margin-right: 20px;
}
}
.regions {
border-top: 1px solid var(--color-separator);
margin-top: 15px;
padding-top: 15px;
}
</style>
<script>
import {
BatteryIcon,
@ -188,3 +164,27 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.device {
display: inline-block;
position: relative;
top: -5px;
color: var(--color-primary);
font-weight: bold;
}
.wrapper {
display: flex;
margin-top: 10px;
img {
align-self: start;
margin-right: 20px;
}
}
.regions {
border-top: 1px solid var(--color-separator);
margin-top: 15px;
padding-top: 15px;
}
</style>

View File

@ -2,12 +2,6 @@
<div />
</template>
<style scoped>
div {
display: none;
}
</style>
<script>
// See https://github.com/KoRiGaN/Vue2Leaflet/blob/e0cf0f29bc519f0a70f0f1eb6e579f947e7ea4ce/src/utils/utils.js
// to understand the `custom` attribute of each prop, how the `set<Prop>`
@ -136,3 +130,9 @@ export default {
},
};
</script>
<style scoped>
div {
display: none;
}
</style>

View File

@ -3,9 +3,9 @@
<pre class="data"><code>{{ data }}</code></pre>
<div class="options">
<input
id="option-minify-json"
v-model="options.minifyJson"
type="checkbox"
id="option-minify-json"
/>
<label for="option-minify-json">
{{ $t("Minify JSON") }}
@ -30,33 +30,6 @@
</modal>
</template>
<style lang="scss" scoped>
.data {
max-height: 300px;
}
.options {
margin-top: 30px;
}
.buttons {
display: flex;
margin-top: 30px;
button {
flex: 1;
&:first-child {
margin-right: 10px;
}
&:last-child {
margin-left: 10px;
}
}
}
</style>
<script>
import { mapState } from "vuex";
import copy from "clipboard-copy";
@ -108,3 +81,30 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.data {
max-height: 300px;
}
.options {
margin-top: 30px;
}
.buttons {
display: flex;
margin-top: 30px;
button {
flex: 1;
&:first-child {
margin-right: 10px;
}
&:last-child {
margin-left: 10px;
}
}
}
</style>

View File

@ -13,6 +13,20 @@
</modal>
</template>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>
<style scoped>
.loader-icon {
animation: spinning 2s linear infinite;
@ -33,17 +47,3 @@
}
}
</style>
<script>
import { mapState } from "vuex";
import { LoaderIcon } from "vue-feather-icons";
export default {
components: {
LoaderIcon,
},
computed: {
...mapState(["requestAbortController"]),
},
};
</script>

View File

@ -3,17 +3,29 @@ import VueI18n from "vue-i18n";
import config from "@/config";
// TODO: This should be possible to do with https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n,
// but that breaks at runtime - may only work with vue-i18n@9?
import da_DK from "@/locales/da-DK.json";
import de_DE from "@/locales/de-DE.json";
import en_GB from "@/locales/en-GB.json";
import en_US from "@/locales/en-US.json";
import es_ES from "@/locales/es-ES.json";
import fr_FR from "@/locales/fr-FR.json";
import sk_SK from "@/locales/sk-SK.json";
import tr_TR from "@/locales/tr-TR.json";
Vue.use(VueI18n);
const locales = require.context("./locales", true, /[A-Za-z0-9-_,\s]+\.json$/i);
const messages = {};
locales.keys().forEach((key) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i);
if (matched && matched.length > 1) {
const locale = matched[1];
messages[locale] = locales(key);
}
});
const messages = {
da_DK: da_DK,
de_DE: de_DE,
en_GB: en_GB,
en_US: en_US,
es_ES: es_ES,
fr_FR: fr_FR,
sk_SK: sk_SK,
tr_TR: tr_TR,
};
export default new VueI18n({
locale: config.locale,

View File

@ -11,7 +11,7 @@ Vue.use(Vuex);
export default new Vuex.Store({
state: {
isLoading: false,
frontendVersion: process.env.PACKAGE_VERSION,
frontendVersion: import.meta.env.PACKAGE_VERSION,
recorderVersion: "",
users: [],
devices: {},

View File

@ -14,14 +14,14 @@
<LControlScale
v-if="controls.scale.display"
:position="controls.scale.position"
:maxWidth="controls.scale.maxWidth"
:max-width="controls.scale.maxWidth"
:metric="controls.scale.metric"
:imperial="controls.scale.imperial"
/>
<LTileLayer
:url="url"
:attribution="attribution"
:tileSize="tileSize"
:tile-size="tileSize"
:options="{ maxNativeZoom, maxZoom, zoomOffset }"
/>
@ -134,8 +134,8 @@ import {
import "leaflet/dist/leaflet.css";
import * as types from "@/store/mutation-types";
import LCustomMarker from "@/components/LCustomMarker";
import LHeatmap from "@/components/LHeatmap";
import LDeviceLocationPopup from "@/components/LDeviceLocationPopup";
import LHeatmap from "@/components/LHeatmap.vue";
import LDeviceLocationPopup from "@/components/LDeviceLocationPopup.vue";
export default {
components: {
@ -179,11 +179,6 @@ export default {
},
};
},
mounted() {
this.$root.$on("fitView", () => {
this.fitView();
});
},
computed: {
...mapGetters([
"filteredLocationHistory",
@ -192,6 +187,21 @@ export default {
]),
...mapState(["lastLocations", "map"]),
},
watch: {
lastLocations() {
if (this.$config.onLocationChange.fitView) {
this.fitView();
}
},
filteredLocationHistory() {
this.fitView();
},
},
mounted() {
this.$root.$on("fitView", () => {
this.fitView();
});
},
methods: {
...mapMutations({
setMapCenter: types.SET_MAP_CENTER,
@ -241,15 +251,5 @@ export default {
}));
},
},
watch: {
lastLocations() {
if (this.$config.onLocationChange.fitView) {
this.fitView();
}
},
filteredLocationHistory() {
this.fitView();
},
},
};
</script>

View File

@ -1,5 +0,0 @@
module.exports = {
env: {
jest: true,
},
};

View File

@ -1,32 +1,42 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";
import * as api from "@/api";
const fetchMocker = createFetchMock(vi);
describe("API", () => {
beforeEach(() => {
fetch.resetMocks();
fetchMocker.enableMocks();
fetchMocker.resetMocks();
});
test("getVersion", async () => {
fetch.mockResponse(JSON.stringify({ version: "1.2.3" }));
fetchMocker.mockResponse(JSON.stringify({ version: "1.2.3" }));
const version = await api.getVersion();
expect(version).toBe("1.2.3");
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/version");
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/version"
);
});
test("getUsers", async () => {
fetch.mockResponse(JSON.stringify({ results: ["foo", "bar"] }));
fetchMocker.mockResponse(JSON.stringify({ results: ["foo", "bar"] }));
const users = await api.getUsers();
expect(users).toEqual(["foo", "bar"]);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/list");
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/list"
);
});
test("getDevices", async () => {
fetch.mockResponses(
fetchMocker.mockResponses(
[JSON.stringify({ results: ["phone", "tablet"] })],
[JSON.stringify({ results: ["laptop"] })]
);
@ -34,12 +44,12 @@ describe("API", () => {
const devices = await api.getDevices(["foo", "bar"]);
expect(devices).toEqual({ foo: ["phone", "tablet"], bar: ["laptop"] });
expect(fetch.mock.calls.length).toEqual(2);
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/list?user=foo"
expect(fetchMocker.mock.calls.length).toEqual(2);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/list?user=foo"
);
expect(fetch.mock.calls[1][0]).toEqual(
"http://localhost/api/0/list?user=bar"
expect(fetchMocker.mock.calls[1][0]).toEqual(
"http://localhost:3000/api/0/list?user=bar"
);
});
@ -60,13 +70,15 @@ describe("API", () => {
disptst: "1970-01-01 00:00:00",
},
];
fetch.mockResponse(JSON.stringify(response));
fetchMocker.mockResponse(JSON.stringify(response));
const lastLocation = await api.getLastLocations();
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual("http://localhost/api/0/last");
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/last"
);
});
test("getLastLocations with user", async () => {
@ -81,14 +93,14 @@ describe("API", () => {
device: "tablet",
},
];
fetch.mockResponse(JSON.stringify(response));
fetchMocker.mockResponse(JSON.stringify(response));
const lastLocation = await api.getLastLocations("foo");
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/last?user=foo"
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/last?user=foo"
);
});
@ -100,14 +112,14 @@ describe("API", () => {
device: "phone",
},
];
fetch.mockResponse(JSON.stringify(response));
fetchMocker.mockResponse(JSON.stringify(response));
const lastLocation = await api.getLastLocations("foo", "phone");
expect(lastLocation).toEqual(response);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/last?user=foo&device=phone"
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/last?user=foo&device=phone"
);
});
@ -137,7 +149,7 @@ describe("API", () => {
],
status: 200,
};
fetch.mockResponse(JSON.stringify(response));
fetchMocker.mockResponse(JSON.stringify(response));
const locationHistory = await api.getUserDeviceLocationHistory(
"foo",
@ -147,14 +159,14 @@ describe("API", () => {
);
expect(locationHistory).toEqual(response.data);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
expect(fetchMocker.mock.calls.length).toEqual(1);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
);
});
test("getLocationHistory", async () => {
fetch.mockResponses(
fetchMocker.mockResponses(
[
JSON.stringify({
count: 1,
@ -203,15 +215,15 @@ describe("API", () => {
bar: { laptop: [{ topic: "owntracks/bar/laptop" }] },
});
expect(fetch.mock.calls.length).toEqual(3);
expect(fetch.mock.calls[0][0]).toEqual(
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
expect(fetchMocker.mock.calls.length).toEqual(3);
expect(fetchMocker.mock.calls[0][0]).toEqual(
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=phone&format=json"
);
expect(fetch.mock.calls[1][0]).toEqual(
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=tablet&format=json"
expect(fetchMocker.mock.calls[1][0]).toEqual(
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=foo&device=tablet&format=json"
);
expect(fetch.mock.calls[2][0]).toEqual(
"http://localhost/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=bar&device=laptop&format=json"
expect(fetchMocker.mock.calls[2][0]).toEqual(
"http://localhost:3000/api/0/locations?from=1970-01-01T00%3A00%3A00&to=1970-12-31T23%3A59%3A59&user=bar&device=laptop&format=json"
);
});
});

View File

@ -1,3 +1,5 @@
import { describe, expect, test } from "vitest";
import config from "@/config";
import {
getApiUrl,
@ -10,9 +12,9 @@ import {
describe("getApiUrl", () => {
test("without base URL", () => {
// See testURL in jest.config.js
expect(getApiUrl("foo").href).toBe("http://localhost/foo");
expect(getApiUrl("/foo").href).toBe("http://localhost/foo");
expect(getApiUrl("/foo/bar").href).toBe("http://localhost/foo/bar");
expect(getApiUrl("foo").href).toBe("http://localhost:3000/foo");
expect(getApiUrl("/foo").href).toBe("http://localhost:3000/foo");
expect(getApiUrl("/foo/bar").href).toBe("http://localhost:3000/foo/bar");
});
test("with base URL", () => {

18
vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2";
import version from "vite-plugin-package-version";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), version()],
resolve: {
alias: {
"@": resolve(dirname(fileURLToPath(import.meta.url)), "./src"),
},
},
test: {
environment: "jsdom",
},
});

View File

@ -1,26 +0,0 @@
const fs = require("fs");
const webpack = require("webpack");
const MomentLocalesPlugin = require("moment-locales-webpack-plugin");
const packageJson = fs.readFileSync("./package.json");
const version = JSON.parse(packageJson).version;
module.exports = {
publicPath: "",
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
"process.env.PACKAGE_VERSION": `"${version}"`,
}),
new MomentLocalesPlugin(),
],
},
pluginOptions: {
i18n: {
locale: "en",
fallbackLocale: "en",
localeDir: "locales",
},
},
};