dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study

- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
This commit is contained in:
Bernt
2026-07-14 15:27:33 +00:00
parent 3c522e39f0
commit 1a12fb870b
6296 changed files with 911440 additions and 55607 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-Present Vitest Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+79
View File
@@ -0,0 +1,79 @@
# @vitest/snapshot
Lightweight implementation of Jest's snapshots.
## Usage
```js
import { SnapshotClient } from '@vitest/snapshot'
import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'
import { SnapshotManager } from '@vitest/snapshot/manager'
const client = new SnapshotClient({
// you need to provide your own equality check implementation if you use it
// this function is called when `.toMatchSnapshot({ property: 1 })` is called
isEqual: (received, expected) => equals(received, expected, [iterableEquality, subsetEquality]),
})
// class that implements snapshot saving and reading
// by default uses fs module, but you can provide your own implementation depending on the environment
const environment = new NodeSnapshotEnvironment()
// you need to implement this yourselves,
// this depends on your runner
function getCurrentFilepath() {
return '/file.spec.js'
}
function getCurrentTestName() {
return 'test1'
}
// example for inline snapshots, nothing is required to support regular snapshots,
// just call `assert` with `isInline: false`
function wrapper(received) {
function __INLINE_SNAPSHOT__(inlineSnapshot, message) {
client.assert({
received,
message,
isInline: true,
inlineSnapshot,
filepath: getCurrentFilepath(),
name: getCurrentTestName(),
})
}
return {
// the name is hard-coded, it should be inside another function, so Vitest can find the actual test file where it was called (parses call stack trace + 2)
// you can override this behaviour in SnapshotState's `_inferInlineSnapshotStack` method by providing your own SnapshotState to SnapshotClient constructor
toMatchInlineSnapshot: (...args) => __INLINE_SNAPSHOT__(...args),
}
}
const options = {
updateSnapshot: 'new',
snapshotEnvironment: environment,
}
await client.startCurrentRun(getCurrentFilepath(), getCurrentTestName(), options)
// this will save snapshot to a file which is returned by "snapshotEnvironment.resolvePath"
client.assert({
received: 'some text',
isInline: false,
})
// uses "pretty-format", so it requires quotes
// also naming is hard-coded when parsing test files
wrapper('text 1').toMatchInlineSnapshot()
wrapper('text 2').toMatchInlineSnapshot('"text 2"')
const result = await client.finishCurrentRun() // this saves files and returns SnapshotResult
// you can use manager to manage several clients
const manager = new SnapshotManager(options)
manager.add(result)
// do something
// and then read the summary
console.log(manager.summary)
```
@@ -0,0 +1,14 @@
interface SnapshotEnvironment {
getVersion: () => string;
getHeader: () => string;
resolvePath: (filepath: string) => Promise<string>;
resolveRawPath: (testPath: string, rawPath: string) => Promise<string>;
saveSnapshotFile: (filepath: string, snapshot: string) => Promise<void>;
readSnapshotFile: (filepath: string) => Promise<string | null>;
removeSnapshotFile: (filepath: string) => Promise<void>;
}
interface SnapshotEnvironmentOptions {
snapshotsDirName?: string;
}
export type { SnapshotEnvironment as S, SnapshotEnvironmentOptions as a };
@@ -0,0 +1,16 @@
import { S as SnapshotEnvironment, a as SnapshotEnvironmentOptions } from './environment-cMiGIVXz.js';
declare class NodeSnapshotEnvironment implements SnapshotEnvironment {
private options;
constructor(options?: SnapshotEnvironmentOptions);
getVersion(): string;
getHeader(): string;
resolveRawPath(testPath: string, rawPath: string): Promise<string>;
resolvePath(filepath: string): Promise<string>;
prepareDirectory(dirPath: string): Promise<void>;
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>;
readSnapshotFile(filepath: string): Promise<string | null>;
removeSnapshotFile(filepath: string): Promise<void>;
}
export { NodeSnapshotEnvironment, SnapshotEnvironment };
@@ -0,0 +1,44 @@
import { promises, existsSync } from 'node:fs';
import { isAbsolute, resolve, dirname, join, basename } from 'pathe';
class NodeSnapshotEnvironment {
constructor(options = {}) {
this.options = options;
}
getVersion() {
return "1";
}
getHeader() {
return `// Snapshot v${this.getVersion()}`;
}
async resolveRawPath(testPath, rawPath) {
return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath);
}
async resolvePath(filepath) {
return join(
join(
dirname(filepath),
this.options.snapshotsDirName ?? "__snapshots__"
),
`${basename(filepath)}.snap`
);
}
async prepareDirectory(dirPath) {
await promises.mkdir(dirPath, { recursive: true });
}
async saveSnapshotFile(filepath, snapshot) {
await promises.mkdir(dirname(filepath), { recursive: true });
await promises.writeFile(filepath, snapshot, "utf-8");
}
async readSnapshotFile(filepath) {
if (!existsSync(filepath))
return null;
return promises.readFile(filepath, "utf-8");
}
async removeSnapshotFile(filepath) {
if (existsSync(filepath))
await promises.unlink(filepath);
}
}
export { NodeSnapshotEnvironment };
@@ -0,0 +1,60 @@
import { Plugin, OptionsReceived } from 'pretty-format';
import { S as SnapshotEnvironment } from './environment-cMiGIVXz.js';
interface RawSnapshotInfo {
file: string;
readonly?: boolean;
content?: string;
}
type SnapshotData = Record<string, string>;
type SnapshotUpdateState = 'all' | 'new' | 'none';
type SnapshotSerializer = Plugin;
interface SnapshotStateOptions {
updateSnapshot: SnapshotUpdateState;
snapshotEnvironment: SnapshotEnvironment;
expand?: boolean;
snapshotFormat?: OptionsReceived;
resolveSnapshotPath?: (path: string, extension: string) => string;
}
interface SnapshotMatchOptions {
testName: string;
received: unknown;
key?: string;
inlineSnapshot?: string;
isInline: boolean;
error?: Error;
rawSnapshot?: RawSnapshotInfo;
}
interface SnapshotResult {
filepath: string;
added: number;
fileDeleted: boolean;
matched: number;
unchecked: number;
uncheckedKeys: Array<string>;
unmatched: number;
updated: number;
}
interface UncheckedSnapshot {
filePath: string;
keys: Array<string>;
}
interface SnapshotSummary {
added: number;
didUpdate: boolean;
failure: boolean;
filesAdded: number;
filesRemoved: number;
filesRemovedList: Array<string>;
filesUnmatched: number;
filesUpdated: number;
matched: number;
total: number;
unchecked: number;
uncheckedKeysByFile: Array<UncheckedSnapshot>;
unmatched: number;
updated: number;
}
export type { RawSnapshotInfo as R, SnapshotStateOptions as S, UncheckedSnapshot as U, SnapshotMatchOptions as a, SnapshotResult as b, SnapshotData as c, SnapshotUpdateState as d, SnapshotSerializer as e, SnapshotSummary as f };
+109
View File
@@ -0,0 +1,109 @@
import { S as SnapshotStateOptions, a as SnapshotMatchOptions, b as SnapshotResult, R as RawSnapshotInfo } from './index-S94ASl6q.js';
export { c as SnapshotData, e as SnapshotSerializer, f as SnapshotSummary, d as SnapshotUpdateState, U as UncheckedSnapshot } from './index-S94ASl6q.js';
import { S as SnapshotEnvironment } from './environment-cMiGIVXz.js';
import { Plugin, Plugins } from 'pretty-format';
interface ParsedStack {
method: string;
file: string;
line: number;
column: number;
}
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
interface SnapshotReturnOptions {
actual: string;
count: number;
expected?: string;
key: string;
pass: boolean;
}
interface SaveStatus {
deleted: boolean;
saved: boolean;
}
declare class SnapshotState {
testFilePath: string;
snapshotPath: string;
private _counters;
private _dirty;
private _updateSnapshot;
private _snapshotData;
private _initialData;
private _inlineSnapshots;
private _rawSnapshots;
private _uncheckedKeys;
private _snapshotFormat;
private _environment;
private _fileExists;
added: number;
expand: boolean;
matched: number;
unmatched: number;
updated: number;
private constructor();
static create(testFilePath: string, options: SnapshotStateOptions): Promise<SnapshotState>;
get environment(): SnapshotEnvironment;
markSnapshotsAsCheckedForTest(testName: string): void;
protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null;
private _addSnapshot;
clear(): void;
save(): Promise<SaveStatus>;
getUncheckedCount(): number;
getUncheckedKeys(): Array<string>;
removeUncheckedKeys(): void;
match({ testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions;
pack(): Promise<SnapshotResult>;
}
interface AssertOptions {
received: unknown;
filepath?: string;
name?: string;
message?: string;
isInline?: boolean;
properties?: object;
inlineSnapshot?: string;
error?: Error;
errorMessage?: string;
rawSnapshot?: RawSnapshotInfo;
}
interface SnapshotClientOptions {
isEqual?: (received: unknown, expected: unknown) => boolean;
}
declare class SnapshotClient {
private options;
filepath?: string;
name?: string;
snapshotState: SnapshotState | undefined;
snapshotStateMap: Map<string, SnapshotState>;
constructor(options?: SnapshotClientOptions);
startCurrentRun(filepath: string, name: string, options: SnapshotStateOptions): Promise<void>;
getSnapshotState(filepath: string): SnapshotState;
clearTest(): void;
skipTestSnapshots(name: string): void;
assert(options: AssertOptions): void;
assertRaw(options: AssertOptions): Promise<void>;
finishCurrentRun(): Promise<SnapshotResult | null>;
clear(): void;
}
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
declare function addSerializer(plugin: Plugin): void;
declare function getSerializers(): Plugins;
declare function stripSnapshotIndentation(inlineSnapshot: string): string;
export { SnapshotClient, SnapshotMatchOptions, SnapshotResult, SnapshotState, SnapshotStateOptions, addSerializer, getSerializers, stripSnapshotIndentation };
+1548
View File
@@ -0,0 +1,1548 @@
import { plugins, format } from 'pretty-format';
import { resolve as resolve$2 } from 'pathe';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var naturalCompare$2 = {exports: {}};
/*
* @version 1.4.0
* @date 2015-10-26
* @stability 3 - Stable
* @author Lauri Rooden (https://github.com/litejs/natural-compare-lite)
* @license MIT License
*/
var naturalCompare = function(a, b) {
var i, codeA
, codeB = 1
, posA = 0
, posB = 0
, alphabet = String.alphabet;
function getCode(str, pos, code) {
if (code) {
for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i;
return +str.slice(pos - 1, i)
}
code = alphabet && alphabet.indexOf(str.charAt(pos));
return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code
: code < 46 ? 65 // -
: code < 48 ? code - 1
: code < 58 ? code + 18 // 0-9
: code < 65 ? code - 11
: code < 91 ? code + 11 // A-Z
: code < 97 ? code - 37
: code < 123 ? code + 5 // a-z
: code - 63
}
if ((a+="") != (b+="")) for (;codeB;) {
codeA = getCode(a, posA++);
codeB = getCode(b, posB++);
if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) {
codeA = getCode(a, posA, posA);
codeB = getCode(b, posB, posA = i);
posB = i;
}
if (codeA != codeB) return (codeA < codeB) ? -1 : 1
}
return 0
};
try {
naturalCompare$2.exports = naturalCompare;
} catch (e) {
String.naturalCompare = naturalCompare;
}
var naturalCompareExports = naturalCompare$2.exports;
var naturalCompare$1 = /*@__PURE__*/getDefaultExportFromCjs(naturalCompareExports);
function notNullish(v) {
return v != null;
}
function isPrimitive(value) {
return value === null || typeof value !== "function" && typeof value !== "object";
}
function isObject(item) {
return item != null && typeof item === "object" && !Array.isArray(item);
}
function getCallLastIndex(code) {
let charIndex = -1;
let inString = null;
let startedBracers = 0;
let endedBracers = 0;
let beforeChar = null;
while (charIndex <= code.length) {
beforeChar = code[charIndex];
charIndex++;
const char = code[charIndex];
const isCharString = char === '"' || char === "'" || char === "`";
if (isCharString && beforeChar !== "\\") {
if (inString === char)
inString = null;
else if (!inString)
inString = char;
}
if (!inString) {
if (char === "(")
startedBracers++;
if (char === ")")
endedBracers++;
}
if (startedBracers && endedBracers && startedBracers === endedBracers)
return charIndex;
}
return null;
}
let getPromiseValue = () => 'Promise{…}';
try {
const { getPromiseDetails, kPending, kRejected } = process.binding('util');
if (Array.isArray(getPromiseDetails(Promise.resolve()))) {
getPromiseValue = (value, options) => {
const [state, innerValue] = getPromiseDetails(value);
if (state === kPending) {
return 'Promise{<pending>}'
}
return `Promise${state === kRejected ? '!' : ''}{${options.inspect(innerValue, options)}}`
};
}
} catch (notNode) {
/* ignore */
}
/* !
* loupe
* Copyright(c) 2013 Jake Luer <jake@alogicalparadox.com>
* MIT Licensed
*/
let nodeInspect = false;
try {
// eslint-disable-next-line global-require
const nodeUtil = require('util');
nodeInspect = nodeUtil.inspect ? nodeUtil.inspect.custom : false;
} catch (noNodeInspect) {
nodeInspect = false;
}
const lineSplitRE = /\r?\n/;
function positionToOffset(source, lineNumber, columnNumber) {
const lines = source.split(lineSplitRE);
const nl = /\r\n/.test(source) ? 2 : 1;
let start = 0;
if (lineNumber > lines.length)
return source.length;
for (let i = 0; i < lineNumber - 1; i++)
start += lines[i].length + nl;
return start + columnNumber;
}
function offsetToLineNumber(source, offset) {
if (offset > source.length) {
throw new Error(
`offset is longer than source length! offset ${offset} > length ${source.length}`
);
}
const lines = source.split(lineSplitRE);
const nl = /\r\n/.test(source) ? 2 : 1;
let counted = 0;
let line = 0;
for (; line < lines.length; line++) {
const lineLength = lines[line].length + nl;
if (counted + lineLength >= offset)
break;
counted += lineLength;
}
return line + 1;
}
// Copyright 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Simon Lydell
// License: MIT.
var LineTerminatorSequence;
LineTerminatorSequence = /\r?\n|[\r\u2028\u2029]/y;
RegExp(LineTerminatorSequence.source);
// src/index.ts
var reservedWords = {
keyword: [
"break",
"case",
"catch",
"continue",
"debugger",
"default",
"do",
"else",
"finally",
"for",
"function",
"if",
"return",
"switch",
"throw",
"try",
"var",
"const",
"while",
"with",
"new",
"this",
"super",
"class",
"extends",
"export",
"import",
"null",
"true",
"false",
"in",
"instanceof",
"typeof",
"void",
"delete"
],
strict: [
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
"yield"
]
}; new Set(reservedWords.keyword); new Set(reservedWords.strict);
const serialize$1 = (val, config, indentation, depth, refs, printer) => {
const name = val.getMockName();
const nameString = name === "vi.fn()" ? "" : ` ${name}`;
let callsString = "";
if (val.mock.calls.length !== 0) {
const indentationNext = indentation + config.indent;
callsString = ` {${config.spacingOuter}${indentationNext}"calls": ${printer(val.mock.calls, config, indentationNext, depth, refs)}${config.min ? ", " : ","}${config.spacingOuter}${indentationNext}"results": ${printer(val.mock.results, config, indentationNext, depth, refs)}${config.min ? "" : ","}${config.spacingOuter}${indentation}}`;
}
return `[MockFunction${nameString}]${callsString}`;
};
const test = (val) => val && !!val._isMockFunction;
const plugin = { serialize: serialize$1, test };
const {
DOMCollection,
DOMElement,
Immutable,
ReactElement,
ReactTestComponent,
AsymmetricMatcher
} = plugins;
let PLUGINS = [
ReactTestComponent,
ReactElement,
DOMElement,
DOMCollection,
Immutable,
AsymmetricMatcher,
plugin
];
function addSerializer(plugin) {
PLUGINS = [plugin].concat(PLUGINS);
}
function getSerializers() {
return PLUGINS;
}
function testNameToKey(testName, count) {
return `${testName} ${count}`;
}
function keyToTestName(key) {
if (!/ \d+$/.test(key))
throw new Error("Snapshot keys must end with a number.");
return key.replace(/ \d+$/, "");
}
function getSnapshotData(content, options) {
const update = options.updateSnapshot;
const data = /* @__PURE__ */ Object.create(null);
let snapshotContents = "";
let dirty = false;
if (content != null) {
try {
snapshotContents = content;
const populate = new Function("exports", snapshotContents);
populate(data);
} catch {
}
}
const isInvalid = snapshotContents;
if ((update === "all" || update === "new") && isInvalid)
dirty = true;
return { data, dirty };
}
function addExtraLineBreaks(string) {
return string.includes("\n") ? `
${string}
` : string;
}
function removeExtraLineBreaks(string) {
return string.length > 2 && string.startsWith("\n") && string.endsWith("\n") ? string.slice(1, -1) : string;
}
const escapeRegex = true;
const printFunctionName = false;
function serialize(val, indent = 2, formatOverrides = {}) {
return normalizeNewlines(
format(val, {
escapeRegex,
indent,
plugins: getSerializers(),
printFunctionName,
...formatOverrides
})
);
}
function escapeBacktickString(str) {
return str.replace(/`|\\|\${/g, "\\$&");
}
function printBacktickString(str) {
return `\`${escapeBacktickString(str)}\``;
}
function normalizeNewlines(string) {
return string.replace(/\r\n|\r/g, "\n");
}
async function saveSnapshotFile(environment, snapshotData, snapshotPath) {
const snapshots = Object.keys(snapshotData).sort(naturalCompare$1).map(
(key) => `exports[${printBacktickString(key)}] = ${printBacktickString(normalizeNewlines(snapshotData[key]))};`
);
const content = `${environment.getHeader()}
${snapshots.join("\n\n")}
`;
const oldContent = await environment.readSnapshotFile(snapshotPath);
const skipWriting = oldContent != null && oldContent === content;
if (skipWriting)
return;
await environment.saveSnapshotFile(
snapshotPath,
content
);
}
function prepareExpected(expected) {
function findStartIndent() {
var _a, _b;
const matchObject = /^( +)}\s+$/m.exec(expected || "");
const objectIndent = (_a = matchObject == null ? void 0 : matchObject[1]) == null ? void 0 : _a.length;
if (objectIndent)
return objectIndent;
const matchText = /^\n( +)"/.exec(expected || "");
return ((_b = matchText == null ? void 0 : matchText[1]) == null ? void 0 : _b.length) || 0;
}
const startIndent = findStartIndent();
let expectedTrimmed = expected == null ? void 0 : expected.trim();
if (startIndent) {
expectedTrimmed = expectedTrimmed == null ? void 0 : expectedTrimmed.replace(new RegExp(`^${" ".repeat(startIndent)}`, "gm"), "").replace(/ +}$/, "}");
}
return expectedTrimmed;
}
function deepMergeArray(target = [], source = []) {
const mergedOutput = Array.from(target);
source.forEach((sourceElement, index) => {
const targetElement = mergedOutput[index];
if (Array.isArray(target[index])) {
mergedOutput[index] = deepMergeArray(target[index], sourceElement);
} else if (isObject(targetElement)) {
mergedOutput[index] = deepMergeSnapshot(target[index], sourceElement);
} else {
mergedOutput[index] = sourceElement;
}
});
return mergedOutput;
}
function deepMergeSnapshot(target, source) {
if (isObject(target) && isObject(source)) {
const mergedOutput = { ...target };
Object.keys(source).forEach((key) => {
if (isObject(source[key]) && !source[key].$$typeof) {
if (!(key in target))
Object.assign(mergedOutput, { [key]: source[key] });
else
mergedOutput[key] = deepMergeSnapshot(target[key], source[key]);
} else if (Array.isArray(source[key])) {
mergedOutput[key] = deepMergeArray(target[key], source[key]);
} else {
Object.assign(mergedOutput, { [key]: source[key] });
}
});
return mergedOutput;
} else if (Array.isArray(target) && Array.isArray(source)) {
return deepMergeArray(target, source);
}
return target;
}
const comma = ','.charCodeAt(0);
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const intToChar = new Uint8Array(64); // 64 possible chars.
const charToInt = new Uint8Array(128); // z is 122 in ASCII
for (let i = 0; i < chars.length; i++) {
const c = chars.charCodeAt(i);
intToChar[i] = c;
charToInt[c] = i;
}
function decode(mappings) {
const state = new Int32Array(5);
const decoded = [];
let index = 0;
do {
const semi = indexOf(mappings, index);
const line = [];
let sorted = true;
let lastCol = 0;
state[0] = 0;
for (let i = index; i < semi; i++) {
let seg;
i = decodeInteger(mappings, i, state, 0); // genColumn
const col = state[0];
if (col < lastCol)
sorted = false;
lastCol = col;
if (hasMoreVlq(mappings, i, semi)) {
i = decodeInteger(mappings, i, state, 1); // sourcesIndex
i = decodeInteger(mappings, i, state, 2); // sourceLine
i = decodeInteger(mappings, i, state, 3); // sourceColumn
if (hasMoreVlq(mappings, i, semi)) {
i = decodeInteger(mappings, i, state, 4); // namesIndex
seg = [col, state[1], state[2], state[3], state[4]];
}
else {
seg = [col, state[1], state[2], state[3]];
}
}
else {
seg = [col];
}
line.push(seg);
}
if (!sorted)
sort(line);
decoded.push(line);
index = semi + 1;
} while (index <= mappings.length);
return decoded;
}
function indexOf(mappings, index) {
const idx = mappings.indexOf(';', index);
return idx === -1 ? mappings.length : idx;
}
function decodeInteger(mappings, pos, state, j) {
let value = 0;
let shift = 0;
let integer = 0;
do {
const c = mappings.charCodeAt(pos++);
integer = charToInt[c];
value |= (integer & 31) << shift;
shift += 5;
} while (integer & 32);
const shouldNegate = value & 1;
value >>>= 1;
if (shouldNegate) {
value = -0x80000000 | -value;
}
state[j] += value;
return pos;
}
function hasMoreVlq(mappings, i, length) {
if (i >= length)
return false;
return mappings.charCodeAt(i) !== comma;
}
function sort(line) {
line.sort(sortComparator$1);
}
function sortComparator$1(a, b) {
return a[0] - b[0];
}
// Matches the scheme of a URL, eg "http://"
const schemeRegex = /^[\w+.-]+:\/\//;
/**
* Matches the parts of a URL:
* 1. Scheme, including ":", guaranteed.
* 2. User/password, including "@", optional.
* 3. Host, guaranteed.
* 4. Port, including ":", optional.
* 5. Path, including "/", optional.
* 6. Query, including "?", optional.
* 7. Hash, including "#", optional.
*/
const urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/;
/**
* File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start
* with a leading `/`, they can have a domain (but only if they don't start with a Windows drive).
*
* 1. Host, optional.
* 2. Path, which may include "/", guaranteed.
* 3. Query, including "?", optional.
* 4. Hash, including "#", optional.
*/
const fileRegex = /^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;
var UrlType;
(function (UrlType) {
UrlType[UrlType["Empty"] = 1] = "Empty";
UrlType[UrlType["Hash"] = 2] = "Hash";
UrlType[UrlType["Query"] = 3] = "Query";
UrlType[UrlType["RelativePath"] = 4] = "RelativePath";
UrlType[UrlType["AbsolutePath"] = 5] = "AbsolutePath";
UrlType[UrlType["SchemeRelative"] = 6] = "SchemeRelative";
UrlType[UrlType["Absolute"] = 7] = "Absolute";
})(UrlType || (UrlType = {}));
function isAbsoluteUrl(input) {
return schemeRegex.test(input);
}
function isSchemeRelativeUrl(input) {
return input.startsWith('//');
}
function isAbsolutePath(input) {
return input.startsWith('/');
}
function isFileUrl(input) {
return input.startsWith('file:');
}
function isRelative(input) {
return /^[.?#]/.test(input);
}
function parseAbsoluteUrl(input) {
const match = urlRegex.exec(input);
return makeUrl(match[1], match[2] || '', match[3], match[4] || '', match[5] || '/', match[6] || '', match[7] || '');
}
function parseFileUrl(input) {
const match = fileRegex.exec(input);
const path = match[2];
return makeUrl('file:', '', match[1] || '', '', isAbsolutePath(path) ? path : '/' + path, match[3] || '', match[4] || '');
}
function makeUrl(scheme, user, host, port, path, query, hash) {
return {
scheme,
user,
host,
port,
path,
query,
hash,
type: UrlType.Absolute,
};
}
function parseUrl(input) {
if (isSchemeRelativeUrl(input)) {
const url = parseAbsoluteUrl('http:' + input);
url.scheme = '';
url.type = UrlType.SchemeRelative;
return url;
}
if (isAbsolutePath(input)) {
const url = parseAbsoluteUrl('http://foo.com' + input);
url.scheme = '';
url.host = '';
url.type = UrlType.AbsolutePath;
return url;
}
if (isFileUrl(input))
return parseFileUrl(input);
if (isAbsoluteUrl(input))
return parseAbsoluteUrl(input);
const url = parseAbsoluteUrl('http://foo.com/' + input);
url.scheme = '';
url.host = '';
url.type = input
? input.startsWith('?')
? UrlType.Query
: input.startsWith('#')
? UrlType.Hash
: UrlType.RelativePath
: UrlType.Empty;
return url;
}
function stripPathFilename(path) {
// If a path ends with a parent directory "..", then it's a relative path with excess parent
// paths. It's not a file, so we can't strip it.
if (path.endsWith('/..'))
return path;
const index = path.lastIndexOf('/');
return path.slice(0, index + 1);
}
function mergePaths(url, base) {
normalizePath(base, base.type);
// If the path is just a "/", then it was an empty path to begin with (remember, we're a relative
// path).
if (url.path === '/') {
url.path = base.path;
}
else {
// Resolution happens relative to the base path's directory, not the file.
url.path = stripPathFilename(base.path) + url.path;
}
}
/**
* The path can have empty directories "//", unneeded parents "foo/..", or current directory
* "foo/.". We need to normalize to a standard representation.
*/
function normalizePath(url, type) {
const rel = type <= UrlType.RelativePath;
const pieces = url.path.split('/');
// We need to preserve the first piece always, so that we output a leading slash. The item at
// pieces[0] is an empty string.
let pointer = 1;
// Positive is the number of real directories we've output, used for popping a parent directory.
// Eg, "foo/bar/.." will have a positive 2, and we can decrement to be left with just "foo".
let positive = 0;
// We need to keep a trailing slash if we encounter an empty directory (eg, splitting "foo/" will
// generate `["foo", ""]` pieces). And, if we pop a parent directory. But once we encounter a
// real directory, we won't need to append, unless the other conditions happen again.
let addTrailingSlash = false;
for (let i = 1; i < pieces.length; i++) {
const piece = pieces[i];
// An empty directory, could be a trailing slash, or just a double "//" in the path.
if (!piece) {
addTrailingSlash = true;
continue;
}
// If we encounter a real directory, then we don't need to append anymore.
addTrailingSlash = false;
// A current directory, which we can always drop.
if (piece === '.')
continue;
// A parent directory, we need to see if there are any real directories we can pop. Else, we
// have an excess of parents, and we'll need to keep the "..".
if (piece === '..') {
if (positive) {
addTrailingSlash = true;
positive--;
pointer--;
}
else if (rel) {
// If we're in a relativePath, then we need to keep the excess parents. Else, in an absolute
// URL, protocol relative URL, or an absolute path, we don't need to keep excess.
pieces[pointer++] = piece;
}
continue;
}
// We've encountered a real directory. Move it to the next insertion pointer, which accounts for
// any popped or dropped directories.
pieces[pointer++] = piece;
positive++;
}
let path = '';
for (let i = 1; i < pointer; i++) {
path += '/' + pieces[i];
}
if (!path || (addTrailingSlash && !path.endsWith('/..'))) {
path += '/';
}
url.path = path;
}
/**
* Attempts to resolve `input` URL/path relative to `base`.
*/
function resolve$1(input, base) {
if (!input && !base)
return '';
const url = parseUrl(input);
let inputType = url.type;
if (base && inputType !== UrlType.Absolute) {
const baseUrl = parseUrl(base);
const baseType = baseUrl.type;
switch (inputType) {
case UrlType.Empty:
url.hash = baseUrl.hash;
// fall through
case UrlType.Hash:
url.query = baseUrl.query;
// fall through
case UrlType.Query:
case UrlType.RelativePath:
mergePaths(url, baseUrl);
// fall through
case UrlType.AbsolutePath:
// The host, user, and port are joined, you can't copy one without the others.
url.user = baseUrl.user;
url.host = baseUrl.host;
url.port = baseUrl.port;
// fall through
case UrlType.SchemeRelative:
// The input doesn't have a schema at least, so we need to copy at least that over.
url.scheme = baseUrl.scheme;
}
if (baseType > inputType)
inputType = baseType;
}
normalizePath(url, inputType);
const queryHash = url.query + url.hash;
switch (inputType) {
// This is impossible, because of the empty checks at the start of the function.
// case UrlType.Empty:
case UrlType.Hash:
case UrlType.Query:
return queryHash;
case UrlType.RelativePath: {
// The first char is always a "/", and we need it to be relative.
const path = url.path.slice(1);
if (!path)
return queryHash || '.';
if (isRelative(base || input) && !isRelative(path)) {
// If base started with a leading ".", or there is no base and input started with a ".",
// then we need to ensure that the relative path starts with a ".". We don't know if
// relative starts with a "..", though, so check before prepending.
return './' + path + queryHash;
}
return path + queryHash;
}
case UrlType.AbsolutePath:
return url.path + queryHash;
default:
return url.scheme + '//' + url.user + url.host + url.port + url.path + queryHash;
}
}
function resolve(input, base) {
// The base is always treated as a directory, if it's not empty.
// https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327
// https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401
if (base && !base.endsWith('/'))
base += '/';
return resolve$1(input, base);
}
/**
* Removes everything after the last "/", but leaves the slash.
*/
function stripFilename(path) {
if (!path)
return '';
const index = path.lastIndexOf('/');
return path.slice(0, index + 1);
}
const COLUMN = 0;
const SOURCES_INDEX = 1;
const SOURCE_LINE = 2;
const SOURCE_COLUMN = 3;
const NAMES_INDEX = 4;
function maybeSort(mappings, owned) {
const unsortedIndex = nextUnsortedSegmentLine(mappings, 0);
if (unsortedIndex === mappings.length)
return mappings;
// If we own the array (meaning we parsed it from JSON), then we're free to directly mutate it. If
// not, we do not want to modify the consumer's input array.
if (!owned)
mappings = mappings.slice();
for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) {
mappings[i] = sortSegments(mappings[i], owned);
}
return mappings;
}
function nextUnsortedSegmentLine(mappings, start) {
for (let i = start; i < mappings.length; i++) {
if (!isSorted(mappings[i]))
return i;
}
return mappings.length;
}
function isSorted(line) {
for (let j = 1; j < line.length; j++) {
if (line[j][COLUMN] < line[j - 1][COLUMN]) {
return false;
}
}
return true;
}
function sortSegments(line, owned) {
if (!owned)
line = line.slice();
return line.sort(sortComparator);
}
function sortComparator(a, b) {
return a[COLUMN] - b[COLUMN];
}
let found = false;
/**
* A binary search implementation that returns the index if a match is found.
* If no match is found, then the left-index (the index associated with the item that comes just
* before the desired index) is returned. To maintain proper sort order, a splice would happen at
* the next index:
*
* ```js
* const array = [1, 3];
* const needle = 2;
* const index = binarySearch(array, needle, (item, needle) => item - needle);
*
* assert.equal(index, 0);
* array.splice(index + 1, 0, needle);
* assert.deepEqual(array, [1, 2, 3]);
* ```
*/
function binarySearch(haystack, needle, low, high) {
while (low <= high) {
const mid = low + ((high - low) >> 1);
const cmp = haystack[mid][COLUMN] - needle;
if (cmp === 0) {
found = true;
return mid;
}
if (cmp < 0) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
found = false;
return low - 1;
}
function upperBound(haystack, needle, index) {
for (let i = index + 1; i < haystack.length; index = i++) {
if (haystack[i][COLUMN] !== needle)
break;
}
return index;
}
function lowerBound(haystack, needle, index) {
for (let i = index - 1; i >= 0; index = i--) {
if (haystack[i][COLUMN] !== needle)
break;
}
return index;
}
function memoizedState() {
return {
lastKey: -1,
lastNeedle: -1,
lastIndex: -1,
};
}
/**
* This overly complicated beast is just to record the last tested line/column and the resulting
* index, allowing us to skip a few tests if mappings are monotonically increasing.
*/
function memoizedBinarySearch(haystack, needle, state, key) {
const { lastKey, lastNeedle, lastIndex } = state;
let low = 0;
let high = haystack.length - 1;
if (key === lastKey) {
if (needle === lastNeedle) {
found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle;
return lastIndex;
}
if (needle >= lastNeedle) {
// lastIndex may be -1 if the previous needle was not found.
low = lastIndex === -1 ? 0 : lastIndex;
}
else {
high = lastIndex;
}
}
state.lastKey = key;
state.lastNeedle = needle;
return (state.lastIndex = binarySearch(haystack, needle, low, high));
}
const LINE_GTR_ZERO = '`line` must be greater than 0 (lines start at line 1)';
const COL_GTR_EQ_ZERO = '`column` must be greater than or equal to 0 (columns start at column 0)';
const LEAST_UPPER_BOUND = -1;
const GREATEST_LOWER_BOUND = 1;
/**
* Returns the decoded (array of lines of segments) form of the SourceMap's mappings field.
*/
let decodedMappings;
/**
* A higher-level API to find the source/line/column associated with a generated line/column
* (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
* `source-map` library.
*/
let originalPositionFor;
class TraceMap {
constructor(map, mapUrl) {
const isString = typeof map === 'string';
if (!isString && map._decodedMemo)
return map;
const parsed = (isString ? JSON.parse(map) : map);
const { version, file, names, sourceRoot, sources, sourcesContent } = parsed;
this.version = version;
this.file = file;
this.names = names || [];
this.sourceRoot = sourceRoot;
this.sources = sources;
this.sourcesContent = sourcesContent;
const from = resolve(sourceRoot || '', stripFilename(mapUrl));
this.resolvedSources = sources.map((s) => resolve(s || '', from));
const { mappings } = parsed;
if (typeof mappings === 'string') {
this._encoded = mappings;
this._decoded = undefined;
}
else {
this._encoded = undefined;
this._decoded = maybeSort(mappings, isString);
}
this._decodedMemo = memoizedState();
this._bySources = undefined;
this._bySourceMemos = undefined;
}
}
(() => {
decodedMappings = (map) => {
return (map._decoded || (map._decoded = decode(map._encoded)));
};
originalPositionFor = (map, { line, column, bias }) => {
line--;
if (line < 0)
throw new Error(LINE_GTR_ZERO);
if (column < 0)
throw new Error(COL_GTR_EQ_ZERO);
const decoded = decodedMappings(map);
// It's common for parent source maps to have pointers to lines that have no
// mapping (like a "//# sourceMappingURL=") at the end of the child file.
if (line >= decoded.length)
return OMapping(null, null, null, null);
const segments = decoded[line];
const index = traceSegmentInternal(segments, map._decodedMemo, line, column, bias || GREATEST_LOWER_BOUND);
if (index === -1)
return OMapping(null, null, null, null);
const segment = segments[index];
if (segment.length === 1)
return OMapping(null, null, null, null);
const { names, resolvedSources } = map;
return OMapping(resolvedSources[segment[SOURCES_INDEX]], segment[SOURCE_LINE] + 1, segment[SOURCE_COLUMN], segment.length === 5 ? names[segment[NAMES_INDEX]] : null);
};
})();
function OMapping(source, line, column, name) {
return { source, line, column, name };
}
function traceSegmentInternal(segments, memo, line, column, bias) {
let index = memoizedBinarySearch(segments, column, memo, line);
if (found) {
index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index);
}
else if (bias === LEAST_UPPER_BOUND)
index++;
if (index === -1 || index === segments.length)
return -1;
return index;
}
const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/;
const stackIgnorePatterns = [
"node:internal",
/\/packages\/\w+\/dist\//,
/\/@vitest\/\w+\/dist\//,
"/vitest/dist/",
"/vitest/src/",
"/vite-node/dist/",
"/vite-node/src/",
"/node_modules/chai/",
"/node_modules/tinypool/",
"/node_modules/tinyspy/",
"/deps/chai.js",
/__vitest_browser__/
];
function extractLocation(urlLike) {
if (!urlLike.includes(":"))
return [urlLike];
const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
const parts = regExp.exec(urlLike.replace(/^\(|\)$/g, ""));
if (!parts)
return [urlLike];
let url = parts[1];
if (url.startsWith("http:") || url.startsWith("https:")) {
const urlObj = new URL(url);
url = urlObj.pathname;
}
if (url.startsWith("/@fs/")) {
url = url.slice(typeof process !== "undefined" && process.platform === "win32" ? 5 : 4);
}
return [url, parts[2] || void 0, parts[3] || void 0];
}
function parseSingleFFOrSafariStack(raw) {
let line = raw.trim();
if (SAFARI_NATIVE_CODE_REGEXP.test(line))
return null;
if (line.includes(" > eval"))
line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");
if (!line.includes("@") && !line.includes(":"))
return null;
const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/;
const matches = line.match(functionNameRegex);
const functionName = matches && matches[1] ? matches[1] : void 0;
const [url, lineNumber, columnNumber] = extractLocation(line.replace(functionNameRegex, ""));
if (!url || !lineNumber || !columnNumber)
return null;
return {
file: url,
method: functionName || "",
line: Number.parseInt(lineNumber),
column: Number.parseInt(columnNumber)
};
}
function parseSingleV8Stack(raw) {
let line = raw.trim();
if (!CHROME_IE_STACK_REGEXP.test(line))
return null;
if (line.includes("(eval "))
line = line.replace(/eval code/g, "eval").replace(/(\(eval at [^()]*)|(,.*$)/g, "");
let sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "(").replace(/^.*?\s+/, "");
const location = sanitizedLine.match(/ (\(.+\)$)/);
sanitizedLine = location ? sanitizedLine.replace(location[0], "") : sanitizedLine;
const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine);
let method = location && sanitizedLine || "";
let file = url && ["eval", "<anonymous>"].includes(url) ? void 0 : url;
if (!file || !lineNumber || !columnNumber)
return null;
if (method.startsWith("async "))
method = method.slice(6);
if (file.startsWith("file://"))
file = file.slice(7);
file = resolve$2(file);
if (method)
method = method.replace(/__vite_ssr_import_\d+__\./g, "");
return {
method,
file,
line: Number.parseInt(lineNumber),
column: Number.parseInt(columnNumber)
};
}
function parseStacktrace(stack, options = {}) {
const { ignoreStackEntries = stackIgnorePatterns } = options;
let stacks = !CHROME_IE_STACK_REGEXP.test(stack) ? parseFFOrSafariStackTrace(stack) : parseV8Stacktrace(stack);
if (ignoreStackEntries.length)
stacks = stacks.filter((stack2) => !ignoreStackEntries.some((p) => stack2.file.match(p)));
return stacks.map((stack2) => {
var _a;
const map = (_a = options.getSourceMap) == null ? void 0 : _a.call(options, stack2.file);
if (!map || typeof map !== "object" || !map.version)
return stack2;
const traceMap = new TraceMap(map);
const { line, column } = originalPositionFor(traceMap, stack2);
if (line != null && column != null)
return { ...stack2, line, column };
return stack2;
});
}
function parseFFOrSafariStackTrace(stack) {
return stack.split("\n").map((line) => parseSingleFFOrSafariStack(line)).filter(notNullish);
}
function parseV8Stacktrace(stack) {
return stack.split("\n").map((line) => parseSingleV8Stack(line)).filter(notNullish);
}
function parseErrorStacktrace(e, options = {}) {
if (!e || isPrimitive(e))
return [];
if (e.stacks)
return e.stacks;
const stackStr = e.stack || e.stackStr || "";
let stackFrames = parseStacktrace(stackStr, options);
if (options.frameFilter)
stackFrames = stackFrames.filter((f) => options.frameFilter(e, f) !== false);
e.stacks = stackFrames;
return stackFrames;
}
async function saveInlineSnapshots(environment, snapshots) {
const MagicString = (await import('magic-string')).default;
const files = new Set(snapshots.map((i) => i.file));
await Promise.all(Array.from(files).map(async (file) => {
const snaps = snapshots.filter((i) => i.file === file);
const code = await environment.readSnapshotFile(file);
const s = new MagicString(code);
for (const snap of snaps) {
const index = positionToOffset(code, snap.line, snap.column);
replaceInlineSnap(code, s, index, snap.snapshot);
}
const transformed = s.toString();
if (transformed !== code)
await environment.saveSnapshotFile(file, transformed);
}));
}
const startObjectRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\S\s]*\*\/\s*|\/\/.*\s+)*\s*({)/m;
function replaceObjectSnap(code, s, index, newSnap) {
let _code = code.slice(index);
const startMatch = startObjectRegex.exec(_code);
if (!startMatch)
return false;
_code = _code.slice(startMatch.index);
let callEnd = getCallLastIndex(_code);
if (callEnd === null)
return false;
callEnd += index + startMatch.index;
const shapeStart = index + startMatch.index + startMatch[0].length;
const shapeEnd = getObjectShapeEndIndex(code, shapeStart);
const snap = `, ${prepareSnapString(newSnap, code, index)}`;
if (shapeEnd === callEnd) {
s.appendLeft(callEnd, snap);
} else {
s.overwrite(shapeEnd, callEnd, snap);
}
return true;
}
function getObjectShapeEndIndex(code, index) {
let startBraces = 1;
let endBraces = 0;
while (startBraces !== endBraces && index < code.length) {
const s = code[index++];
if (s === "{")
startBraces++;
else if (s === "}")
endBraces++;
}
return index;
}
function prepareSnapString(snap, source, index) {
const lineNumber = offsetToLineNumber(source, index);
const line = source.split(lineSplitRE)[lineNumber - 1];
const indent = line.match(/^\s*/)[0] || "";
const indentNext = indent.includes(" ") ? `${indent} ` : `${indent} `;
const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
const isOneline = lines.length <= 1;
const quote = "`";
if (isOneline)
return `${quote}${lines.join("\n").replace(/`/g, "\\`").replace(/\${/g, "\\${")}${quote}`;
return `${quote}
${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\${/g, "\\${")}
${indent}${quote}`;
}
const startRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\S\s]*\*\/\s*|\/\/.*\s+)*\s*[\w_$]*(['"`\)])/m;
function replaceInlineSnap(code, s, index, newSnap) {
const codeStartingAtIndex = code.slice(index);
const startMatch = startRegex.exec(codeStartingAtIndex);
const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(codeStartingAtIndex);
if (!startMatch || startMatch.index !== (firstKeywordMatch == null ? void 0 : firstKeywordMatch.index))
return replaceObjectSnap(code, s, index, newSnap);
const quote = startMatch[1];
const startIndex = index + startMatch.index + startMatch[0].length;
const snapString = prepareSnapString(newSnap, code, index);
if (quote === ")") {
s.appendRight(startIndex - 1, snapString);
return true;
}
const quoteEndRE = new RegExp(`(?:^|[^\\\\])${quote}`);
const endMatch = quoteEndRE.exec(code.slice(startIndex));
if (!endMatch)
return false;
const endIndex = startIndex + endMatch.index + endMatch[0].length;
s.overwrite(startIndex - 1, endIndex, snapString);
return true;
}
const INDENTATION_REGEX = /^([^\S\n]*)\S/m;
function stripSnapshotIndentation(inlineSnapshot) {
const match = inlineSnapshot.match(INDENTATION_REGEX);
if (!match || !match[1]) {
return inlineSnapshot;
}
const indentation = match[1];
const lines = inlineSnapshot.split(/\n/g);
if (lines.length <= 2) {
return inlineSnapshot;
}
if (lines[0].trim() !== "" || lines[lines.length - 1].trim() !== "") {
return inlineSnapshot;
}
for (let i = 1; i < lines.length - 1; i++) {
if (lines[i] !== "") {
if (lines[i].indexOf(indentation) !== 0) {
return inlineSnapshot;
}
lines[i] = lines[i].substring(indentation.length);
}
}
lines[lines.length - 1] = "";
inlineSnapshot = lines.join("\n");
return inlineSnapshot;
}
async function saveRawSnapshots(environment, snapshots) {
await Promise.all(snapshots.map(async (snap) => {
if (!snap.readonly)
await environment.saveSnapshotFile(snap.file, snap.snapshot);
}));
}
class SnapshotState {
constructor(testFilePath, snapshotPath, snapshotContent, options) {
this.testFilePath = testFilePath;
this.snapshotPath = snapshotPath;
const { data, dirty } = getSnapshotData(
snapshotContent,
options
);
this._fileExists = snapshotContent != null;
this._initialData = data;
this._snapshotData = data;
this._dirty = dirty;
this._inlineSnapshots = [];
this._rawSnapshots = [];
this._uncheckedKeys = new Set(Object.keys(this._snapshotData));
this._counters = /* @__PURE__ */ new Map();
this.expand = options.expand || false;
this.added = 0;
this.matched = 0;
this.unmatched = 0;
this._updateSnapshot = options.updateSnapshot;
this.updated = 0;
this._snapshotFormat = {
printBasicPrototype: false,
escapeString: false,
...options.snapshotFormat
};
this._environment = options.snapshotEnvironment;
}
_counters;
_dirty;
_updateSnapshot;
_snapshotData;
_initialData;
_inlineSnapshots;
_rawSnapshots;
_uncheckedKeys;
_snapshotFormat;
_environment;
_fileExists;
added;
expand;
matched;
unmatched;
updated;
static async create(testFilePath, options) {
const snapshotPath = await options.snapshotEnvironment.resolvePath(testFilePath);
const content = await options.snapshotEnvironment.readSnapshotFile(snapshotPath);
return new SnapshotState(testFilePath, snapshotPath, content, options);
}
get environment() {
return this._environment;
}
markSnapshotsAsCheckedForTest(testName) {
this._uncheckedKeys.forEach((uncheckedKey) => {
if (keyToTestName(uncheckedKey) === testName)
this._uncheckedKeys.delete(uncheckedKey);
});
}
_inferInlineSnapshotStack(stacks) {
const promiseIndex = stacks.findIndex((i) => i.method.match(/__VITEST_(RESOLVES|REJECTS)__/));
if (promiseIndex !== -1)
return stacks[promiseIndex + 3];
const stackIndex = stacks.findIndex((i) => i.method.includes("__INLINE_SNAPSHOT__"));
return stackIndex !== -1 ? stacks[stackIndex + 2] : null;
}
_addSnapshot(key, receivedSerialized, options) {
this._dirty = true;
if (options.isInline) {
const stacks = parseErrorStacktrace(options.error || new Error("snapshot"), { ignoreStackEntries: [] });
const stack = this._inferInlineSnapshotStack(stacks);
if (!stack) {
throw new Error(
`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.
${JSON.stringify(stacks)}`
);
}
stack.column--;
this._inlineSnapshots.push({
snapshot: receivedSerialized,
...stack
});
} else if (options.rawSnapshot) {
this._rawSnapshots.push({
...options.rawSnapshot,
snapshot: receivedSerialized
});
} else {
this._snapshotData[key] = receivedSerialized;
}
}
clear() {
this._snapshotData = this._initialData;
this._counters = /* @__PURE__ */ new Map();
this.added = 0;
this.matched = 0;
this.unmatched = 0;
this.updated = 0;
this._dirty = false;
}
async save() {
const hasExternalSnapshots = Object.keys(this._snapshotData).length;
const hasInlineSnapshots = this._inlineSnapshots.length;
const hasRawSnapshots = this._rawSnapshots.length;
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots;
const status = {
deleted: false,
saved: false
};
if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) {
if (hasExternalSnapshots) {
await saveSnapshotFile(this._environment, this._snapshotData, this.snapshotPath);
this._fileExists = true;
}
if (hasInlineSnapshots)
await saveInlineSnapshots(this._environment, this._inlineSnapshots);
if (hasRawSnapshots)
await saveRawSnapshots(this._environment, this._rawSnapshots);
status.saved = true;
} else if (!hasExternalSnapshots && this._fileExists) {
if (this._updateSnapshot === "all") {
await this._environment.removeSnapshotFile(this.snapshotPath);
this._fileExists = false;
}
status.deleted = true;
}
return status;
}
getUncheckedCount() {
return this._uncheckedKeys.size || 0;
}
getUncheckedKeys() {
return Array.from(this._uncheckedKeys);
}
removeUncheckedKeys() {
if (this._updateSnapshot === "all" && this._uncheckedKeys.size) {
this._dirty = true;
this._uncheckedKeys.forEach((key) => delete this._snapshotData[key]);
this._uncheckedKeys.clear();
}
}
match({
testName,
received,
key,
inlineSnapshot,
isInline,
error,
rawSnapshot
}) {
this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
const count = Number(this._counters.get(testName));
if (!key)
key = testNameToKey(testName, count);
if (!(isInline && this._snapshotData[key] !== void 0))
this._uncheckedKeys.delete(key);
let receivedSerialized = rawSnapshot && typeof received === "string" ? received : serialize(received, void 0, this._snapshotFormat);
if (!rawSnapshot)
receivedSerialized = addExtraLineBreaks(receivedSerialized);
if (rawSnapshot) {
if (rawSnapshot.content && rawSnapshot.content.match(/\r\n/) && !receivedSerialized.match(/\r\n/))
rawSnapshot.content = normalizeNewlines(rawSnapshot.content);
}
const expected = isInline ? inlineSnapshot : rawSnapshot ? rawSnapshot.content : this._snapshotData[key];
const expectedTrimmed = prepareExpected(expected);
const pass = expectedTrimmed === prepareExpected(receivedSerialized);
const hasSnapshot = expected !== void 0;
const snapshotIsPersisted = isInline || this._fileExists || rawSnapshot && rawSnapshot.content != null;
if (pass && !isInline && !rawSnapshot) {
this._snapshotData[key] = receivedSerialized;
}
if (hasSnapshot && this._updateSnapshot === "all" || (!hasSnapshot || !snapshotIsPersisted) && (this._updateSnapshot === "new" || this._updateSnapshot === "all")) {
if (this._updateSnapshot === "all") {
if (!pass) {
if (hasSnapshot)
this.updated++;
else
this.added++;
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot });
} else {
this.matched++;
}
} else {
this._addSnapshot(key, receivedSerialized, { error, isInline, rawSnapshot });
this.added++;
}
return {
actual: "",
count,
expected: "",
key,
pass: true
};
} else {
if (!pass) {
this.unmatched++;
return {
actual: removeExtraLineBreaks(receivedSerialized),
count,
expected: expectedTrimmed !== void 0 ? removeExtraLineBreaks(expectedTrimmed) : void 0,
key,
pass: false
};
} else {
this.matched++;
return {
actual: "",
count,
expected: "",
key,
pass: true
};
}
}
}
async pack() {
const snapshot = {
filepath: this.testFilePath,
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0
};
const uncheckedCount = this.getUncheckedCount();
const uncheckedKeys = this.getUncheckedKeys();
if (uncheckedCount)
this.removeUncheckedKeys();
const status = await this.save();
snapshot.fileDeleted = status.deleted;
snapshot.added = this.added;
snapshot.matched = this.matched;
snapshot.unmatched = this.unmatched;
snapshot.updated = this.updated;
snapshot.unchecked = !status.deleted ? uncheckedCount : 0;
snapshot.uncheckedKeys = Array.from(uncheckedKeys);
return snapshot;
}
}
function createMismatchError(message, expand, actual, expected) {
const error = new Error(message);
Object.defineProperty(error, "actual", {
value: actual,
enumerable: true,
configurable: true,
writable: true
});
Object.defineProperty(error, "expected", {
value: expected,
enumerable: true,
configurable: true,
writable: true
});
Object.defineProperty(error, "diffOptions", { value: { expand } });
return error;
}
class SnapshotClient {
constructor(options = {}) {
this.options = options;
}
filepath;
name;
snapshotState;
snapshotStateMap = /* @__PURE__ */ new Map();
async startCurrentRun(filepath, name, options) {
var _a;
this.filepath = filepath;
this.name = name;
if (((_a = this.snapshotState) == null ? void 0 : _a.testFilePath) !== filepath) {
await this.finishCurrentRun();
if (!this.getSnapshotState(filepath)) {
this.snapshotStateMap.set(
filepath,
await SnapshotState.create(
filepath,
options
)
);
}
this.snapshotState = this.getSnapshotState(filepath);
}
}
getSnapshotState(filepath) {
return this.snapshotStateMap.get(filepath);
}
clearTest() {
this.filepath = void 0;
this.name = void 0;
}
skipTestSnapshots(name) {
var _a;
(_a = this.snapshotState) == null ? void 0 : _a.markSnapshotsAsCheckedForTest(name);
}
assert(options) {
var _a, _b, _c, _d;
const {
filepath = this.filepath,
name = this.name,
message,
isInline = false,
properties,
inlineSnapshot,
error,
errorMessage,
rawSnapshot
} = options;
let { received } = options;
if (!filepath)
throw new Error("Snapshot cannot be used outside of test");
if (typeof properties === "object") {
if (typeof received !== "object" || !received)
throw new Error("Received value must be an object when the matcher has properties");
try {
const pass2 = ((_b = (_a = this.options).isEqual) == null ? void 0 : _b.call(_a, received, properties)) ?? false;
if (!pass2)
throw createMismatchError("Snapshot properties mismatched", (_c = this.snapshotState) == null ? void 0 : _c.expand, received, properties);
else
received = deepMergeSnapshot(received, properties);
} catch (err) {
err.message = errorMessage || "Snapshot mismatched";
throw err;
}
}
const testName = [
name,
...message ? [message] : []
].join(" > ");
const snapshotState = this.getSnapshotState(filepath);
const { actual, expected, key, pass } = snapshotState.match({
testName,
received,
isInline,
error,
inlineSnapshot,
rawSnapshot
});
if (!pass)
throw createMismatchError(`Snapshot \`${key || "unknown"}\` mismatched`, (_d = this.snapshotState) == null ? void 0 : _d.expand, actual == null ? void 0 : actual.trim(), expected == null ? void 0 : expected.trim());
}
async assertRaw(options) {
if (!options.rawSnapshot)
throw new Error("Raw snapshot is required");
const {
filepath = this.filepath,
rawSnapshot
} = options;
if (rawSnapshot.content == null) {
if (!filepath)
throw new Error("Snapshot cannot be used outside of test");
const snapshotState = this.getSnapshotState(filepath);
options.filepath || (options.filepath = filepath);
rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file);
rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) || void 0;
}
return this.assert(options);
}
async finishCurrentRun() {
if (!this.snapshotState)
return null;
const result = await this.snapshotState.pack();
this.snapshotState = void 0;
return result;
}
clear() {
this.snapshotStateMap.clear();
}
}
export { SnapshotClient, SnapshotState, addSerializer, getSerializers, stripSnapshotIndentation };
@@ -0,0 +1,18 @@
import { S as SnapshotStateOptions, f as SnapshotSummary, b as SnapshotResult } from './index-S94ASl6q.js';
import 'pretty-format';
import './environment-cMiGIVXz.js';
declare class SnapshotManager {
options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>;
summary: SnapshotSummary;
extension: string;
constructor(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>);
clear(): void;
add(result: SnapshotResult): void;
resolvePath(testPath: string): string;
resolveRawPath(testPath: string, rawPath: string): string;
}
declare function emptySummary(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>): SnapshotSummary;
declare function addSnapshotResult(summary: SnapshotSummary, result: SnapshotResult): void;
export { SnapshotManager, addSnapshotResult, emptySummary };
+75
View File
@@ -0,0 +1,75 @@
import { join, dirname, basename, isAbsolute, resolve } from 'pathe';
class SnapshotManager {
constructor(options) {
this.options = options;
this.clear();
}
summary = void 0;
extension = ".snap";
clear() {
this.summary = emptySummary(this.options);
}
add(result) {
addSnapshotResult(this.summary, result);
}
resolvePath(testPath) {
const resolver = this.options.resolveSnapshotPath || (() => {
return join(
join(
dirname(testPath),
"__snapshots__"
),
`${basename(testPath)}${this.extension}`
);
});
const path = resolver(testPath, this.extension);
return path;
}
resolveRawPath(testPath, rawPath) {
return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath);
}
}
function emptySummary(options) {
const summary = {
added: 0,
failure: false,
filesAdded: 0,
filesRemoved: 0,
filesRemovedList: [],
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
uncheckedKeysByFile: [],
unmatched: 0,
updated: 0,
didUpdate: options.updateSnapshot === "all"
};
return summary;
}
function addSnapshotResult(summary, result) {
if (result.added)
summary.filesAdded++;
if (result.fileDeleted)
summary.filesRemoved++;
if (result.unmatched)
summary.filesUnmatched++;
if (result.updated)
summary.filesUpdated++;
summary.added += result.added;
summary.matched += result.matched;
summary.unchecked += result.unchecked;
if (result.uncheckedKeys && result.uncheckedKeys.length > 0) {
summary.uncheckedKeysByFile.push({
filePath: result.filepath,
keys: result.uncheckedKeys
});
}
summary.unmatched += result.unmatched;
summary.updated += result.updated;
summary.total += result.added + result.matched + result.unmatched + result.updated;
}
export { SnapshotManager, addSnapshotResult, emptySummary };
@@ -0,0 +1 @@
export * from './dist/environment.js'
+1
View File
@@ -0,0 +1 @@
export * from './dist/manager.js'
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@vitest/snapshot",
"type": "module",
"version": "1.6.1",
"description": "Vitest snapshot manager",
"license": "MIT",
"funding": "https://opencollective.com/vitest",
"homepage": "https://github.com/vitest-dev/vitest/tree/main/packages/snapshot#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/vitest-dev/vitest.git",
"directory": "packages/snapshot"
},
"bugs": {
"url": "https://github.com/vitest-dev/vitest/issues"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./environment": {
"types": "./dist/environment.d.ts",
"default": "./dist/environment.js"
},
"./manager": {
"types": "./dist/manager.d.ts",
"default": "./dist/manager.js"
},
"./*": "./*"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"*.d.ts",
"dist"
],
"dependencies": {
"magic-string": "^0.30.5",
"pathe": "^1.1.1",
"pretty-format": "^29.7.0"
},
"devDependencies": {
"@types/natural-compare": "^1.4.3",
"natural-compare": "^1.4.0",
"@vitest/utils": "1.6.1"
},
"scripts": {
"build": "rimraf dist && rollup -c",
"dev": "rollup -c --watch"
}
}