Add progressive web app companion for cross-platform access
Vite + TypeScript PWA that mirrors the Android app's core features: - Pre-processed shelter data (build-time UTM33N→WGS84 conversion) - Leaflet map with shelter markers, user location, and offline tiles - Canvas compass arrow (ported from DirectionArrowView.kt) - IndexedDB shelter cache with 7-day staleness check - Service worker with CacheFirst tiles and precached app shell - i18n for en, nb, nn (ported from Android strings.xml) - iOS/Android compass handling with low-pass filter - Respects user map interaction (no auto-snap on pan/zoom) - Build revision cache-breaker for reliable SW updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46365b713b
commit
e8428de775
12051 changed files with 1799735 additions and 0 deletions
461
pwa/node_modules/vitest/dist/coverage.js
generated
vendored
Normal file
461
pwa/node_modules/vitest/dist/coverage.js
generated
vendored
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
import { existsSync, promises, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { c as coverageConfigDefaults, r as resolveCoverageReporters, m as mm } from './chunks/resolveConfig.rBxzbVsl.js';
|
||||
import { resolve, relative } from 'pathe';
|
||||
import c from 'tinyrainbow';
|
||||
import 'node:crypto';
|
||||
import '@vitest/utils';
|
||||
import 'node:path';
|
||||
import 'node:fs/promises';
|
||||
import 'node:process';
|
||||
import 'node:module';
|
||||
import 'node:url';
|
||||
import 'node:assert';
|
||||
import 'node:v8';
|
||||
import 'node:util';
|
||||
import './chunks/constants.fzPh7AOq.js';
|
||||
import 'node:os';
|
||||
import './chunks/RandomSequencer.CMRlh2v4.js';
|
||||
import 'std-env';
|
||||
import 'node:perf_hooks';
|
||||
import '@vitest/runner/utils';
|
||||
import '@vitest/utils/source-map';
|
||||
import 'tinyexec';
|
||||
import 'vite';
|
||||
import 'fs';
|
||||
import 'vite-node/utils';
|
||||
import './chunks/_commonjsHelpers.BFTU3MAI.js';
|
||||
import 'util';
|
||||
import 'path';
|
||||
import 'node:events';
|
||||
import './chunks/index.68735LiX.js';
|
||||
import 'tinypool';
|
||||
import 'node:worker_threads';
|
||||
import './path.js';
|
||||
|
||||
const THRESHOLD_KEYS = [
|
||||
"lines",
|
||||
"functions",
|
||||
"statements",
|
||||
"branches"
|
||||
];
|
||||
const GLOBAL_THRESHOLDS_KEY = "global";
|
||||
const DEFAULT_PROJECT = Symbol.for("default-project");
|
||||
let uniqueId = 0;
|
||||
class BaseCoverageProvider {
|
||||
ctx;
|
||||
name;
|
||||
version;
|
||||
options;
|
||||
coverageFiles = /* @__PURE__ */ new Map();
|
||||
pendingPromises = [];
|
||||
coverageFilesDirectory;
|
||||
_initialize(ctx) {
|
||||
this.ctx = ctx;
|
||||
if (ctx.version !== this.version) {
|
||||
ctx.logger.warn(
|
||||
c.yellow(
|
||||
`Loaded ${c.inverse(c.yellow(` vitest@${ctx.version} `))} and ${c.inverse(c.yellow(` @vitest/coverage-${this.name}@${this.version} `))}.
|
||||
Running mixed versions is not supported and may lead into bugs
|
||||
Update your dependencies and make sure the versions match.`
|
||||
)
|
||||
);
|
||||
}
|
||||
const config = ctx.config.coverage;
|
||||
this.options = {
|
||||
...coverageConfigDefaults,
|
||||
// User's options
|
||||
...config,
|
||||
// Resolved fields
|
||||
provider: this.name,
|
||||
reportsDirectory: resolve(
|
||||
ctx.config.root,
|
||||
config.reportsDirectory || coverageConfigDefaults.reportsDirectory
|
||||
),
|
||||
reporter: resolveCoverageReporters(
|
||||
config.reporter || coverageConfigDefaults.reporter
|
||||
),
|
||||
thresholds: config.thresholds && {
|
||||
...config.thresholds,
|
||||
lines: config.thresholds["100"] ? 100 : config.thresholds.lines,
|
||||
branches: config.thresholds["100"] ? 100 : config.thresholds.branches,
|
||||
functions: config.thresholds["100"] ? 100 : config.thresholds.functions,
|
||||
statements: config.thresholds["100"] ? 100 : config.thresholds.statements
|
||||
}
|
||||
};
|
||||
const shard = this.ctx.config.shard;
|
||||
const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ""}`;
|
||||
this.coverageFilesDirectory = resolve(
|
||||
this.options.reportsDirectory,
|
||||
tempDirectory
|
||||
);
|
||||
}
|
||||
createCoverageMap() {
|
||||
throw new Error("BaseReporter's createCoverageMap was not overwritten");
|
||||
}
|
||||
async generateReports(_, __) {
|
||||
throw new Error("BaseReporter's generateReports was not overwritten");
|
||||
}
|
||||
async parseConfigModule(_) {
|
||||
throw new Error("BaseReporter's parseConfigModule was not overwritten");
|
||||
}
|
||||
resolveOptions() {
|
||||
return this.options;
|
||||
}
|
||||
async clean(clean = true) {
|
||||
if (clean && existsSync(this.options.reportsDirectory)) {
|
||||
await promises.rm(this.options.reportsDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10
|
||||
});
|
||||
}
|
||||
if (existsSync(this.coverageFilesDirectory)) {
|
||||
await promises.rm(this.coverageFilesDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10
|
||||
});
|
||||
}
|
||||
await promises.mkdir(this.coverageFilesDirectory, { recursive: true });
|
||||
this.coverageFiles = /* @__PURE__ */ new Map();
|
||||
this.pendingPromises = [];
|
||||
}
|
||||
onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }) {
|
||||
if (!coverage) {
|
||||
return;
|
||||
}
|
||||
if (transformMode !== "web" && transformMode !== "ssr" && transformMode !== "browser") {
|
||||
throw new Error(`Invalid transform mode: ${transformMode}`);
|
||||
}
|
||||
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT);
|
||||
if (!entry) {
|
||||
entry = { web: {}, ssr: {}, browser: {} };
|
||||
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry);
|
||||
}
|
||||
const testFilenames = testFiles.join();
|
||||
const filename = resolve(
|
||||
this.coverageFilesDirectory,
|
||||
`coverage-${uniqueId++}.json`
|
||||
);
|
||||
entry[transformMode][testFilenames] = filename;
|
||||
const promise = promises.writeFile(filename, JSON.stringify(coverage), "utf-8");
|
||||
this.pendingPromises.push(promise);
|
||||
}
|
||||
async readCoverageFiles({ onFileRead, onFinished, onDebug }) {
|
||||
let index = 0;
|
||||
const total = this.pendingPromises.length;
|
||||
await Promise.all(this.pendingPromises);
|
||||
this.pendingPromises = [];
|
||||
for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) {
|
||||
for (const [transformMode, coverageByTestfiles] of Object.entries(coveragePerProject)) {
|
||||
const filenames = Object.values(coverageByTestfiles);
|
||||
const project = this.ctx.getProjectByName(projectName);
|
||||
for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) {
|
||||
if (onDebug.enabled) {
|
||||
index += chunk.length;
|
||||
onDebug("Covered files %d/%d", index, total);
|
||||
}
|
||||
await Promise.all(
|
||||
chunk.map(async (filename) => {
|
||||
const contents = await promises.readFile(filename, "utf-8");
|
||||
const coverage = JSON.parse(contents);
|
||||
onFileRead(coverage);
|
||||
})
|
||||
);
|
||||
}
|
||||
await onFinished(project, transformMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
async cleanAfterRun() {
|
||||
this.coverageFiles = /* @__PURE__ */ new Map();
|
||||
await promises.rm(this.coverageFilesDirectory, { recursive: true });
|
||||
if (readdirSync(this.options.reportsDirectory).length === 0) {
|
||||
await promises.rm(this.options.reportsDirectory, { recursive: true });
|
||||
}
|
||||
}
|
||||
async onTestFailure() {
|
||||
if (!this.options.reportOnFailure) {
|
||||
await this.cleanAfterRun();
|
||||
}
|
||||
}
|
||||
async reportCoverage(coverageMap, { allTestsRun }) {
|
||||
await this.generateReports(
|
||||
coverageMap || this.createCoverageMap(),
|
||||
allTestsRun
|
||||
);
|
||||
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch;
|
||||
if (!keepResults) {
|
||||
await this.cleanAfterRun();
|
||||
}
|
||||
}
|
||||
async reportThresholds(coverageMap, allTestsRun) {
|
||||
const resolvedThresholds = this.resolveThresholds(coverageMap);
|
||||
this.checkThresholds(resolvedThresholds);
|
||||
if (this.options.thresholds?.autoUpdate && allTestsRun) {
|
||||
if (!this.ctx.server.config.configFile) {
|
||||
throw new Error(
|
||||
'Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.'
|
||||
);
|
||||
}
|
||||
const configFilePath = this.ctx.server.config.configFile;
|
||||
const configModule = await this.parseConfigModule(configFilePath);
|
||||
await this.updateThresholds({
|
||||
thresholds: resolvedThresholds,
|
||||
configurationFile: configModule,
|
||||
onUpdate: () => writeFileSync(
|
||||
configFilePath,
|
||||
configModule.generate().code,
|
||||
"utf-8"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Constructs collected coverage and users' threshold options into separate sets
|
||||
* where each threshold set holds their own coverage maps. Threshold set is either
|
||||
* for specific files defined by glob pattern or global for all other files.
|
||||
*/
|
||||
resolveThresholds(coverageMap) {
|
||||
const resolvedThresholds = [];
|
||||
const files = coverageMap.files();
|
||||
const globalCoverageMap = this.createCoverageMap();
|
||||
for (const key of Object.keys(this.options.thresholds)) {
|
||||
if (key === "perFile" || key === "autoUpdate" || key === "100" || THRESHOLD_KEYS.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
const glob = key;
|
||||
const globThresholds = resolveGlobThresholds(this.options.thresholds[glob]);
|
||||
const globCoverageMap = this.createCoverageMap();
|
||||
const matchingFiles = files.filter(
|
||||
(file) => mm.isMatch(relative(this.ctx.config.root, file), glob)
|
||||
);
|
||||
for (const file of matchingFiles) {
|
||||
const fileCoverage = coverageMap.fileCoverageFor(file);
|
||||
globCoverageMap.addFileCoverage(fileCoverage);
|
||||
}
|
||||
resolvedThresholds.push({
|
||||
name: glob,
|
||||
coverageMap: globCoverageMap,
|
||||
thresholds: globThresholds
|
||||
});
|
||||
}
|
||||
for (const file of files) {
|
||||
const fileCoverage = coverageMap.fileCoverageFor(file);
|
||||
globalCoverageMap.addFileCoverage(fileCoverage);
|
||||
}
|
||||
resolvedThresholds.unshift({
|
||||
name: GLOBAL_THRESHOLDS_KEY,
|
||||
coverageMap: globalCoverageMap,
|
||||
thresholds: {
|
||||
branches: this.options.thresholds?.branches,
|
||||
functions: this.options.thresholds?.functions,
|
||||
lines: this.options.thresholds?.lines,
|
||||
statements: this.options.thresholds?.statements
|
||||
}
|
||||
});
|
||||
return resolvedThresholds;
|
||||
}
|
||||
/**
|
||||
* Check collected coverage against configured thresholds. Sets exit code to 1 when thresholds not reached.
|
||||
*/
|
||||
checkThresholds(allThresholds) {
|
||||
for (const { coverageMap, thresholds, name } of allThresholds) {
|
||||
if (thresholds.branches === void 0 && thresholds.functions === void 0 && thresholds.lines === void 0 && thresholds.statements === void 0) {
|
||||
continue;
|
||||
}
|
||||
const summaries = this.options.thresholds?.perFile ? coverageMap.files().map((file) => ({
|
||||
file,
|
||||
summary: coverageMap.fileCoverageFor(file).toSummary()
|
||||
})) : [{ file: null, summary: coverageMap.getCoverageSummary() }];
|
||||
for (const { summary, file } of summaries) {
|
||||
for (const thresholdKey of THRESHOLD_KEYS) {
|
||||
const threshold = thresholds[thresholdKey];
|
||||
if (threshold !== void 0) {
|
||||
const coverage = summary.data[thresholdKey].pct;
|
||||
if (coverage < threshold) {
|
||||
process.exitCode = 1;
|
||||
let errorMessage = `ERROR: Coverage for ${thresholdKey} (${coverage}%) does not meet ${name === GLOBAL_THRESHOLDS_KEY ? name : `"${name}"`} threshold (${threshold}%)`;
|
||||
if (this.options.thresholds?.perFile && file) {
|
||||
errorMessage += ` for ${relative("./", file).replace(/\\/g, "/")}`;
|
||||
}
|
||||
this.ctx.logger.error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if current coverage is above configured thresholds and bump the thresholds if needed
|
||||
*/
|
||||
async updateThresholds({ thresholds: allThresholds, onUpdate, configurationFile }) {
|
||||
let updatedThresholds = false;
|
||||
const config = resolveConfig(configurationFile);
|
||||
assertConfigurationModule(config);
|
||||
for (const { coverageMap, thresholds, name } of allThresholds) {
|
||||
const summaries = this.options.thresholds?.perFile ? coverageMap.files().map(
|
||||
(file) => coverageMap.fileCoverageFor(file).toSummary()
|
||||
) : [coverageMap.getCoverageSummary()];
|
||||
const thresholdsToUpdate = [];
|
||||
for (const key of THRESHOLD_KEYS) {
|
||||
const threshold = thresholds[key] ?? 100;
|
||||
const actual = Math.min(
|
||||
...summaries.map((summary) => summary[key].pct)
|
||||
);
|
||||
if (actual > threshold) {
|
||||
thresholdsToUpdate.push([key, actual]);
|
||||
}
|
||||
}
|
||||
if (thresholdsToUpdate.length === 0) {
|
||||
continue;
|
||||
}
|
||||
updatedThresholds = true;
|
||||
for (const [threshold, newValue] of thresholdsToUpdate) {
|
||||
if (name === GLOBAL_THRESHOLDS_KEY) {
|
||||
config.test.coverage.thresholds[threshold] = newValue;
|
||||
} else {
|
||||
const glob = config.test.coverage.thresholds[name];
|
||||
glob[threshold] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatedThresholds) {
|
||||
this.ctx.logger.log("Updating thresholds to configuration file. You may want to push with updated coverage thresholds.");
|
||||
onUpdate();
|
||||
}
|
||||
}
|
||||
async mergeReports(coverageMaps) {
|
||||
const coverageMap = this.createCoverageMap();
|
||||
for (const coverage of coverageMaps) {
|
||||
coverageMap.merge(coverage);
|
||||
}
|
||||
await this.generateReports(coverageMap, true);
|
||||
}
|
||||
hasTerminalReporter(reporters) {
|
||||
return reporters.some(
|
||||
([reporter]) => reporter === "text" || reporter === "text-summary" || reporter === "text-lcov" || reporter === "teamcity"
|
||||
);
|
||||
}
|
||||
toSlices(array, size) {
|
||||
return array.reduce((chunks, item) => {
|
||||
const index = Math.max(0, chunks.length - 1);
|
||||
const lastChunk = chunks[index] || [];
|
||||
chunks[index] = lastChunk;
|
||||
if (lastChunk.length >= size) {
|
||||
chunks.push([item]);
|
||||
} else {
|
||||
lastChunk.push(item);
|
||||
}
|
||||
return chunks;
|
||||
}, []);
|
||||
}
|
||||
createUncoveredFileTransformer(ctx) {
|
||||
const servers = [
|
||||
...ctx.projects.map((project) => ({
|
||||
root: project.config.root,
|
||||
vitenode: project.vitenode
|
||||
})),
|
||||
// Check core last as it will match all files anyway
|
||||
{ root: ctx.config.root, vitenode: ctx.vitenode }
|
||||
];
|
||||
return async function transformFile(filename) {
|
||||
let lastError;
|
||||
for (const { root, vitenode } of servers) {
|
||||
if (!filename.startsWith(root)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return await vitenode.transformRequest(filename);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
};
|
||||
}
|
||||
}
|
||||
function resolveGlobThresholds(thresholds) {
|
||||
if (!thresholds || typeof thresholds !== "object") {
|
||||
return {};
|
||||
}
|
||||
if (100 in thresholds && thresholds[100] === true) {
|
||||
return {
|
||||
lines: 100,
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
statements: 100
|
||||
};
|
||||
}
|
||||
return {
|
||||
lines: "lines" in thresholds && typeof thresholds.lines === "number" ? thresholds.lines : void 0,
|
||||
branches: "branches" in thresholds && typeof thresholds.branches === "number" ? thresholds.branches : void 0,
|
||||
functions: "functions" in thresholds && typeof thresholds.functions === "number" ? thresholds.functions : void 0,
|
||||
statements: "statements" in thresholds && typeof thresholds.statements === "number" ? thresholds.statements : void 0
|
||||
};
|
||||
}
|
||||
function assertConfigurationModule(config) {
|
||||
try {
|
||||
if (typeof config.test.coverage.thresholds !== "object") {
|
||||
throw new TypeError(
|
||||
"Expected config.test.coverage.thresholds to be an object"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Unable to parse thresholds from configuration file: ${message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
function resolveConfig(configModule) {
|
||||
const mod = configModule.exports.default;
|
||||
try {
|
||||
if (mod.$type === "object") {
|
||||
return mod;
|
||||
}
|
||||
let config = resolveDefineConfig(mod);
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
if (mod.$type === "function-call" && mod.$callee === "mergeConfig") {
|
||||
config = resolveMergeConfig(mod);
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
throw new Error(
|
||||
"Failed to update coverage thresholds. Configuration file is too complex."
|
||||
);
|
||||
}
|
||||
function resolveDefineConfig(mod) {
|
||||
if (mod.$type === "function-call" && mod.$callee === "defineConfig") {
|
||||
if (mod.$args[0].$type === "object") {
|
||||
return mod.$args[0];
|
||||
}
|
||||
if (mod.$args[0].$type === "arrow-function-expression") {
|
||||
if (mod.$args[0].$body.$type === "object") {
|
||||
return mod.$args[0].$body;
|
||||
}
|
||||
const config = resolveMergeConfig(mod.$args[0].$body);
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function resolveMergeConfig(mod) {
|
||||
if (mod.$type === "function-call" && mod.$callee === "mergeConfig") {
|
||||
for (const arg of mod.$args) {
|
||||
const config = resolveDefineConfig(arg);
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseCoverageProvider };
|
||||
Loading…
Add table
Add a link
Reference in a new issue