Merged in feature/BOOK-401-handle-new-redirects (pull request #2942)
feat(BOOK-401): redirect-service with tests and typings * Switch to typescript * add new dependencies for tests and typescript * Switch to typescript * add new dependencies for tests and typescript * rewrite to typescript and include tests * rewrite to typescript and include tests * refactor: update README and scripts for CSV handling; rename update script to generate * include csv-data folder Approved-by: Linus Flood
This commit is contained in:
3
apps/scandic-redirect/.gitignore
vendored
3
apps/scandic-redirect/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
scripts/csv/*.csv
|
||||
scripts/json/*.json
|
||||
scripts/data/**/*.csv
|
||||
|
||||
@@ -16,10 +16,10 @@ https://scandichotelsab.sharepoint.com/:x:/s/921-ContentNewweb/ETGStOQAARtJhJXG9
|
||||
- Open it
|
||||
- Each domain/language has its own sheet
|
||||
- Export each sheet into their respective language code
|
||||
- File > Export > Download as CSV UTF-8
|
||||
- Save as [lang].csv in `./scripts/csv` folder
|
||||
- Run the `update` script target
|
||||
- E.g. `yarn workspace @scandic-hotels/scandic-redirect update`
|
||||
- File > Export > Download as CSV UTF-8
|
||||
- Save as [lang].csv in `./scripts/data/csv` folder
|
||||
- Run the `generate` script target
|
||||
- E.g. `yarn workspace @scandic-hotels/scandic-redirect generate`
|
||||
- Commit and push the JSON files in `./netlify/functions/data`.
|
||||
- Create a PR
|
||||
- Profit!
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,15 +1,27 @@
|
||||
{
|
||||
"name": "@scandic-hotels/scandic-redirect",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"dependencies": {
|
||||
"@netlify/functions": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"convert-csv-to-json": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"update": "node ./scripts/update.mjs"
|
||||
}
|
||||
"name": "@scandic-hotels/scandic-redirect",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"generate": "jiti ./scripts/generateRedirectFile/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@netlify/functions": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"convert-csv-to-json": "^3.4.0",
|
||||
"jiti": "^2.6.1",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import fs from "node:fs"
|
||||
import { langs } from "./lang"
|
||||
import { csvFilePath } from "./paths"
|
||||
|
||||
export function checkPrerequisites() {
|
||||
const missingLangs = langs.reduce((acc, lang) => {
|
||||
const filepath = csvFilePath(lang)
|
||||
if (!fs.existsSync(filepath)) {
|
||||
return [...acc, filepath]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[])
|
||||
|
||||
if (missingLangs.length > 0) {
|
||||
throw new Error(`Missing CSV file:\n${missingLangs.join("\n")}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { createRedirectEntry } from "./createRedirectEntry"
|
||||
describe("createRedirectEntry", () => {
|
||||
describe("top level domain from old site to new", () => {
|
||||
it(".com", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.com/old-path",
|
||||
newUrl: "https://www.scandichotels.com/en/new-path",
|
||||
})
|
||||
).toEqual({ from: "/en/old-path", to: "/en/new-path" })
|
||||
})
|
||||
|
||||
it(".se", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.se/old-path",
|
||||
newUrl: "https://www.scandichotels.com/sv/new-path",
|
||||
})
|
||||
).toEqual({ from: "/sv/old-path", to: "/sv/new-path" })
|
||||
})
|
||||
|
||||
it(".dk", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.dk/old-path",
|
||||
newUrl: "https://www.scandichotels.com/da/new-path",
|
||||
})
|
||||
).toEqual({ from: "/da/old-path", to: "/da/new-path" })
|
||||
})
|
||||
|
||||
it(".de", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.de/old-path",
|
||||
newUrl: "https://www.scandichotels.com/de/new-path",
|
||||
})
|
||||
).toEqual({ from: "/de/old-path", to: "/de/new-path" })
|
||||
})
|
||||
|
||||
it(".fi", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.fi/old-path",
|
||||
newUrl: "https://www.scandichotels.com/fi/new-path",
|
||||
})
|
||||
).toEqual({ from: "/fi/old-path", to: "/fi/new-path" })
|
||||
})
|
||||
|
||||
it(".no", () => {
|
||||
expect(
|
||||
createRedirectEntry({
|
||||
oldUrl: "https://scandichotels.no/old-path",
|
||||
newUrl: "https://www.scandichotels.com/no/new-path",
|
||||
})
|
||||
).toEqual({ from: "/no/old-path", to: "/no/new-path" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { akamaiRedirect, removeDomain } from "./utils"
|
||||
|
||||
export function createRedirectEntry({
|
||||
oldUrl,
|
||||
newUrl,
|
||||
}: {
|
||||
oldUrl: string
|
||||
newUrl: string
|
||||
}) {
|
||||
const from = removeDomain(akamaiRedirect(oldUrl))
|
||||
const to = removeDomain(newUrl)
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
}
|
||||
}
|
||||
68
apps/scandic-redirect/scripts/generateRedirectFile/index.ts
Normal file
68
apps/scandic-redirect/scripts/generateRedirectFile/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from "node:fs"
|
||||
|
||||
import csvToJson from "convert-csv-to-json"
|
||||
import { Lang, langs } from "./lang"
|
||||
import { csvFilePath, outputFilepath } from "./paths"
|
||||
import { checkPrerequisites } from "./checkPrerequisites"
|
||||
import { createRedirectEntry } from "./createRedirectEntry"
|
||||
|
||||
const csvHeaders = {
|
||||
current: "Old URL",
|
||||
redirect: "Redirect URL",
|
||||
} as const
|
||||
|
||||
type Entry = {
|
||||
[key in (typeof csvHeaders)[keyof typeof csvHeaders]]: string
|
||||
}
|
||||
|
||||
async function convertCsvToJson<T extends Record<string, unknown>>(
|
||||
lang: Lang
|
||||
): Promise<T[]> {
|
||||
const json = csvToJson
|
||||
.utf8Encoding()
|
||||
.fieldDelimiter(",")
|
||||
.getJsonFromCsv(csvFilePath(lang))
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
async function createRedirectFile(data: Entry[], lang: Lang) {
|
||||
const finalUrls = data.reduce(
|
||||
(acc, url) => {
|
||||
const { from, to } = createRedirectEntry({
|
||||
oldUrl: url[csvHeaders.current],
|
||||
newUrl: url[csvHeaders.redirect],
|
||||
})
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[from]: to,
|
||||
}
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
|
||||
try {
|
||||
fs.writeFileSync(outputFilepath(lang), JSON.stringify(finalUrls), {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`Unable to create redirect file for lang: ${lang}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
checkPrerequisites()
|
||||
console.log("All prerequisites met. Generating redirect files...")
|
||||
let totalCount = 0
|
||||
for (const lang of langs.toSorted()) {
|
||||
const perf = performance.now()
|
||||
const data = await convertCsvToJson<Entry>(lang)
|
||||
await createRedirectFile(data, lang)
|
||||
const duration = (performance.now() - perf).toFixed(0)
|
||||
console.log(
|
||||
`Created redirect file for ${lang}: Processed ${data.length} entries in ${duration}ms`
|
||||
)
|
||||
totalCount += data.length
|
||||
}
|
||||
|
||||
console.log(`\n🥳 All done!\nProcessed a total of ${totalCount} entries.`)
|
||||
@@ -0,0 +1,2 @@
|
||||
export const langs = ["da", "de", "en", "fi", "no", "sv"] as const
|
||||
export type Lang = (typeof langs)[number]
|
||||
17
apps/scandic-redirect/scripts/generateRedirectFile/paths.ts
Normal file
17
apps/scandic-redirect/scripts/generateRedirectFile/paths.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Lang } from "./lang"
|
||||
import path from "node:path"
|
||||
|
||||
export function csvFilePath(lang: Lang) {
|
||||
return `${import.meta.dirname}/../data/csv/${lang}.csv`
|
||||
}
|
||||
|
||||
export function jsonFilePath(lang: Lang) {
|
||||
return `${import.meta.dirname}/../data/json/${lang}.json`
|
||||
}
|
||||
|
||||
export function outputFilepath(lang: Lang) {
|
||||
return path.resolve(
|
||||
import.meta.dirname,
|
||||
`../../netlify/functions/data/${lang}.json`
|
||||
)
|
||||
}
|
||||
114
apps/scandic-redirect/scripts/generateRedirectFile/utils.test.ts
Normal file
114
apps/scandic-redirect/scripts/generateRedirectFile/utils.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { akamaiRedirect, removeDomain } from "./utils"
|
||||
|
||||
describe("removeDomain", () => {
|
||||
it("https scandichotels.com with path", () => {
|
||||
expect(removeDomain("https://scandichotels.com/foo")).toBe("/foo")
|
||||
})
|
||||
|
||||
it("http www scandichotels.com no path", () => {
|
||||
expect(removeDomain("http://www.scandichotels.com")).toBe("")
|
||||
})
|
||||
|
||||
it("stage subdomain with de TLD, query and hash", () => {
|
||||
expect(
|
||||
removeDomain("https://stage.scandichotels.de/path?query=1#frag")
|
||||
).toBe("/path?query=1#frag")
|
||||
})
|
||||
|
||||
it("prod subdomain with fi TLD and trailing slash", () => {
|
||||
expect(removeDomain("http://prod.scandichotels.fi/")).toBe("/")
|
||||
})
|
||||
|
||||
it("test subdomain with dk TLD and deeper path", () => {
|
||||
expect(removeDomain("https://test.scandichotels.dk/some/page")).toBe(
|
||||
"/some/page"
|
||||
)
|
||||
})
|
||||
|
||||
it("no TLD with unicode path", () => {
|
||||
expect(removeDomain("https://scandichotels.no/öä")).toBe("/öä")
|
||||
})
|
||||
|
||||
it("se TLD root", () => {
|
||||
expect(removeDomain("https://scandichotels.se")).toBe("")
|
||||
})
|
||||
|
||||
it("input without protocol should be unchanged", () => {
|
||||
expect(removeDomain("scandichotels.com/foo")).toBe("scandichotels.com/foo")
|
||||
})
|
||||
|
||||
it("different domain containing scandichotels should be unchanged", () => {
|
||||
expect(removeDomain("https://example.com/scandichotels.com/foo")).toBe(
|
||||
"https://example.com/scandichotels.com/foo"
|
||||
)
|
||||
})
|
||||
|
||||
it("uppercase scheme/domain should be unchanged (regex is case-sensitive)", () => {
|
||||
expect(removeDomain("HTTPS://WWW.SCANDICHOTELS.COM/Path")).toBe(
|
||||
"HTTPS://WWW.SCANDICHOTELS.COM/Path"
|
||||
)
|
||||
})
|
||||
|
||||
it("only removes the leading matching domain when it appears at the start", () => {
|
||||
const input = "https://scandichotels.com/https://scandichotels.com/foo"
|
||||
expect(removeDomain(input)).toBe("/https://scandichotels.com/foo")
|
||||
})
|
||||
})
|
||||
|
||||
describe("akamaiRedirect", () => {
|
||||
it("https scandichotels.com with path", () => {
|
||||
expect(akamaiRedirect("https://scandichotels.com/foo")).toBe("/en/foo")
|
||||
})
|
||||
|
||||
it("http www scandichotels.com no path", () => {
|
||||
expect(akamaiRedirect("http://www.scandichotels.com")).toBe("/en")
|
||||
})
|
||||
|
||||
it("stage subdomain with de TLD, query and hash", () => {
|
||||
expect(
|
||||
akamaiRedirect("https://stage.scandichotels.de/path?query=1#frag")
|
||||
).toBe("/de/path?query=1#frag")
|
||||
})
|
||||
|
||||
it("prod subdomain with fi TLD and trailing slash", () => {
|
||||
expect(akamaiRedirect("http://prod.scandichotels.fi/")).toBe("/fi/")
|
||||
})
|
||||
|
||||
it("test subdomain with dk TLD and deeper path", () => {
|
||||
expect(akamaiRedirect("https://test.scandichotels.dk/some/page")).toBe(
|
||||
"/da/some/page"
|
||||
)
|
||||
})
|
||||
|
||||
it("no TLD with unicode path", () => {
|
||||
expect(akamaiRedirect("https://scandichotels.no/öä")).toBe("/no/öä")
|
||||
})
|
||||
|
||||
it("se TLD root", () => {
|
||||
expect(akamaiRedirect("https://scandichotels.se")).toBe("/sv")
|
||||
})
|
||||
|
||||
it("input without protocol should be unchanged", () => {
|
||||
expect(akamaiRedirect("scandichotels.com/foo")).toBe(
|
||||
"scandichotels.com/foo"
|
||||
)
|
||||
})
|
||||
|
||||
it("different domain containing scandichotels should be unchanged", () => {
|
||||
expect(akamaiRedirect("https://example.com/scandichotels.com/foo")).toBe(
|
||||
"https://example.com/scandichotels.com/foo"
|
||||
)
|
||||
})
|
||||
|
||||
it("uppercase scheme/domain should be unchanged (regex is case-sensitive)", () => {
|
||||
expect(akamaiRedirect("HTTPS://WWW.SCANDICHOTELS.COM/Path")).toBe(
|
||||
"HTTPS://WWW.SCANDICHOTELS.COM/Path"
|
||||
)
|
||||
})
|
||||
|
||||
it("only replaces the leading matching domain when it appears at the start", () => {
|
||||
const input = "https://scandichotels.com/https://scandichotels.com/foo"
|
||||
expect(akamaiRedirect(input)).toBe("/en/https://scandichotels.com/foo")
|
||||
})
|
||||
})
|
||||
68
apps/scandic-redirect/scripts/generateRedirectFile/utils.ts
Normal file
68
apps/scandic-redirect/scripts/generateRedirectFile/utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Removes the scheme and Scandic domain prefix from a URL string.
|
||||
*
|
||||
* Strips leading "http://" or "https://" and an optional "www|test|stage|prod" subdomain
|
||||
* for hosts matching scandichotels.(com|de|dk|fi|no|se).
|
||||
*
|
||||
* @param str - Input string or URL.
|
||||
* @returns The input with the Scandic domain portion removed.
|
||||
*/
|
||||
export function removeDomain(str: string) {
|
||||
return str.replace(
|
||||
/^https?:\/\/((www|test|stage|prod)\.)?scandichotels.(com|de|dk|fi|no|se)/,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an absolute Scandic Hotels URL to a localized path prefix based on the top-level domain.
|
||||
*
|
||||
* Replaces the leading "http(s)://[www|test|stage|prod.]scandichotels.{tld}" portion with a locale prefix:
|
||||
* - .com -> /en
|
||||
* - .de -> /de
|
||||
* - .dk -> /da
|
||||
* - .fi -> /fi
|
||||
* - .no -> /no
|
||||
* - .se -> /sv
|
||||
*
|
||||
* The function recognizes http and https schemes and optional subdomains (www, test, stage, prod).
|
||||
* If the input does not match the expected host pattern, the original string is returned unchanged.
|
||||
*
|
||||
* Examples:
|
||||
* - "https://scandichotels.com/hotels" -> "/en/hotels"
|
||||
* - "http://www.scandichotels.de" -> "/de"
|
||||
* - "https://stage.scandichotels.se/?" -> "/sv/?"
|
||||
*
|
||||
* @param str - The URL string to convert.
|
||||
* @returns The input string with the Scandic Hotels host replaced by the locale path prefix,
|
||||
* or the original string if no matching Scandic host is found.
|
||||
*
|
||||
* @remarks
|
||||
* - This is a simple regex-based replacement and does not perform full URL parsing or validation.
|
||||
* - The remainder of the path, query string, and fragment (if present) are preserved after the locale prefix.
|
||||
* - If the original URL ends exactly at the TLD (no trailing slash), the result will be the locale prefix without a trailing slash.
|
||||
*/
|
||||
export function akamaiRedirect(str: string) {
|
||||
return str.replace(
|
||||
/^https?:\/\/((www|test|stage|prod)\.)?scandichotels.(com|de|dk|fi|no|se)/,
|
||||
(...match) => {
|
||||
if (match[3]) {
|
||||
switch (match[3]) {
|
||||
case "com":
|
||||
return "/en"
|
||||
case "de":
|
||||
return "/de"
|
||||
case "dk":
|
||||
return "/da"
|
||||
case "fi":
|
||||
return "/fi"
|
||||
case "no":
|
||||
return "/no"
|
||||
case "se":
|
||||
return "/sv"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import csvToJson from 'convert-csv-to-json';
|
||||
|
||||
const langs = ['da', 'de', 'en', 'fi', 'no', 'sv'];
|
||||
const csvHeaders = {
|
||||
current: 'Current URL',
|
||||
redirect: 'Redirect URL',
|
||||
};
|
||||
|
||||
function csvFilePath(lang) {
|
||||
return `${import.meta.dirname}/csv/${lang}.csv`;
|
||||
}
|
||||
|
||||
function jsonFilePath(lang) {
|
||||
return `${import.meta.dirname}/json/${lang}.json`;
|
||||
}
|
||||
|
||||
function outputFilepath(lang) {
|
||||
return path.resolve(
|
||||
import.meta.dirname,
|
||||
`../netlify/functions/data/${lang}.json`
|
||||
);
|
||||
}
|
||||
|
||||
function removeDomain(str) {
|
||||
return str.replace(
|
||||
/^https?:\/\/((www|test|stage|prod)\.)?scandichotels.(com|de|dk|fi|no|se)/,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function akamaiRedirect(str) {
|
||||
return str.replace(
|
||||
/^https?:\/\/((www|test|stage|prod)\.)?scandichotels.(com|de|dk|fi|no|se)/,
|
||||
(...match) => {
|
||||
if (match[3]) {
|
||||
switch (match[3]) {
|
||||
case 'com':
|
||||
return '/en';
|
||||
case 'de':
|
||||
return '/de';
|
||||
case 'dk':
|
||||
return '/da';
|
||||
case 'fi':
|
||||
return '/fi';
|
||||
case 'no':
|
||||
return '/no';
|
||||
case 'se':
|
||||
return '/sv';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function checkPrerequisites() {
|
||||
const missingLangs = langs.reduce((acc, lang) => {
|
||||
const filepath = csvFilePath(lang);
|
||||
if (!fs.existsSync(filepath)) {
|
||||
acc.push(filepath);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (missingLangs.length > 0) {
|
||||
console.error(`Missing CSV file:\n${missingLangs.join('\n')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// convert-csv-to-json writes async without callback support
|
||||
// so we workaround it be overriding console.log which it uses when it is done
|
||||
async function convertCsvToJson() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const _consoleLog = console.log;
|
||||
let resolved = 0;
|
||||
console.log = function (str) {
|
||||
if (str.indexOf('File saved:') >= 0) {
|
||||
resolved++;
|
||||
}
|
||||
|
||||
if (resolved === langs.length) {
|
||||
console.log = _consoleLog;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
for (const lang of langs) {
|
||||
csvToJson
|
||||
.utf8Encoding()
|
||||
.fieldDelimiter(',')
|
||||
.generateJsonFileFromCsv(csvFilePath(lang), jsonFilePath(lang));
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
reject('timeout');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
async function makeOutput() {
|
||||
for (const lang of langs) {
|
||||
try {
|
||||
const json = JSON.parse(
|
||||
fs.readFileSync(jsonFilePath(lang), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
);
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
const finalUrls = json.reduce((acc, url) => {
|
||||
const from = removeDomain(akamaiRedirect(url[csvHeaders.current]));
|
||||
const to = removeDomain(url[csvHeaders.redirect]);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[from]: to,
|
||||
};
|
||||
}, {});
|
||||
|
||||
fs.writeFileSync(outputFilepath(lang), JSON.stringify(finalUrls), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} else {
|
||||
throw new Error(`JSON was not an array: ${jsonFilePath(lang)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkPrerequisites();
|
||||
await convertCsvToJson();
|
||||
await makeOutput();
|
||||
3
apps/scandic-redirect/vitest-setup.ts
Normal file
3
apps/scandic-redirect/vitest-setup.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { config } from "dotenv"
|
||||
|
||||
config({ path: "./.env.test" })
|
||||
17
apps/scandic-redirect/vitest.config.ts
Normal file
17
apps/scandic-redirect/vitest.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./vitest-setup.ts"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "."),
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user