diff --git a/package-lock.json b/package-lock.json index d34b125..0c22ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,10 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", "axios": "^1.6.7", + "diff": "^9.0.0", + "dompurify": "^3.4.8", "lucide-vue-next": "^0.554.0", + "marked": "^18.0.5", "pinia": "^2.1.7", "primeicons": "^6.0.1", "primevue": "^3.50.0", @@ -1320,6 +1323,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -2218,6 +2228,15 @@ "node": ">=0.4.0" } }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2244,6 +2263,15 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3394,6 +3422,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index fa4896d..1e880f4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,10 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", "axios": "^1.6.7", + "diff": "^9.0.0", + "dompurify": "^3.4.8", "lucide-vue-next": "^0.554.0", + "marked": "^18.0.5", "pinia": "^2.1.7", "primeicons": "^6.0.1", "primevue": "^3.50.0", diff --git a/src/App.vue b/src/App.vue index c934471..2ec13e3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,8 @@ diff --git a/vitest.config.ts b/vitest.config.ts index b7ed526..11ff67e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ test: { environment: "happy-dom", globals: true, + setupFiles: ["./vitest.setup.ts"], }, resolve: { alias: { diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..1d2a1f8 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,34 @@ +// Node 22+ defines global web storage stubs that are non-functional without +// --localstorage-file and shadow happy-dom's implementation, so tests get a +// localStorage whose methods are undefined. Replace both globals with a +// working in-memory implementation. +const createMemoryStorage = (): Storage => { + let store = new Map(); + return { + get length() { + return store.size; + }, + clear: () => { + store = new Map(); + }, + getItem: (key: string) => (store.has(key) ? store.get(key)! : null), + key: (index: number) => Array.from(store.keys())[index] ?? null, + removeItem: (key: string) => { + store.delete(key); + }, + setItem: (key: string, value: string) => { + store.set(key, String(value)); + }, + }; +}; + +for (const name of ["localStorage", "sessionStorage"] as const) { + const storage = createMemoryStorage(); + for (const target of [globalThis, window]) { + Object.defineProperty(target, name, { + value: storage, + writable: true, + configurable: true, + }); + } +}