Standardize monorepo pipeline: test, lint, typecheck across all workspaces
Previously only 2-3 workspaces participated in each turbo task. This expands test, lint, and typecheck to cover all 11 workspaces with parallel execution and caching. Test pipeline: - Move from single root vitest to per-workspace test scripts via turbo - Shared vitest config (vitest.shared.ts) with passWithNoTests - Per-workspace configs re-export the shared base - Mobile uses Jest (jest-expo + React Native Testing Library) - Add node-environment GPX tests verifying linkedom fallback - Add i18n mobile init tests - Exclude apps/mobile from root vitest (uses Jest separately) Lint pipeline: - Add eslint lint script to all 9 previously unlinted workspaces - Standardize all scripts to "eslint ." with shared root config - Add .expo/ to global ESLint ignores - Fix lint errors: unused imports in api/types, export parseGpx Typecheck pipeline: - Add "typecheck": "tsc" to all 8 packages - Add @types/node (catalog) to gpx and db packages - Fix mobile app.config.ts: remove deprecated experiments.monorepo and newArchEnabled (both default in Expo SDK 55) - Add allowImportingTsExtensions to mobile tsconfig Shared package compatibility (mobile-app Phase 1.3): - Add linkedom as explicit dependency to @trails-cool/gpx - Add initI18nMobile() export to @trails-cool/i18n - Confirm @trails-cool/types is pure interfaces (no DOM deps) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
423499d21d
commit
2ac4014521
41 changed files with 1787 additions and 36 deletions
|
|
@ -8,7 +8,8 @@
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"start": "node --experimental-strip-types server.ts",
|
"start": "node --experimental-strip-types server.ts",
|
||||||
"typecheck": "react-router typegen && tsc",
|
"typecheck": "react-router typegen && tsc",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "catalog:",
|
"@react-router/node": "catalog:",
|
||||||
|
|
|
||||||
1
apps/journal/vitest.config.ts
Normal file
1
apps/journal/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -9,7 +9,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
icon: "./assets/icon.png",
|
icon: "./assets/icon.png",
|
||||||
userInterfaceStyle: "light",
|
userInterfaceStyle: "light",
|
||||||
newArchEnabled: true,
|
|
||||||
splash: {
|
splash: {
|
||||||
image: "./assets/splash-icon.png",
|
image: "./assets/splash-icon.png",
|
||||||
resizeMode: "contain",
|
resizeMode: "contain",
|
||||||
|
|
@ -29,8 +28,5 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
web: {
|
web: {
|
||||||
favicon: "./assets/favicon.png",
|
favicon: "./assets/favicon.png",
|
||||||
},
|
},
|
||||||
experiments: {
|
|
||||||
monorepo: true,
|
|
||||||
},
|
|
||||||
plugins: ["expo-router"],
|
plugins: ["expo-router"],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
11
apps/mobile/app/__tests__/smoke.test.tsx
Normal file
11
apps/mobile/app/__tests__/smoke.test.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react-native";
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
describe("Jest + React Native Testing Library", () => {
|
||||||
|
it("renders a component", () => {
|
||||||
|
render(<Text testID="hello">Hello</Text>);
|
||||||
|
expect(screen.getByTestId("hello")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Hello")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "jest-expo",
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!(.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@trails-cool/.*))"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trails-cool/api": "workspace:*",
|
"@trails-cool/api": "workspace:*",
|
||||||
|
|
@ -24,7 +33,10 @@
|
||||||
"react-native-screens": "~4.23.0"
|
"react-native-screens": "~4.23.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/react-native": "^13.3.3",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/react": "~19.2.14",
|
"@types/react": "~19.2.14",
|
||||||
|
"jest-expo": "^55.0.15",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "expo/tsconfig.base",
|
"extends": "expo/tsconfig.base",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"start": "node server.ts",
|
"start": "node server.ts",
|
||||||
"typecheck": "react-router typegen && tsc",
|
"typecheck": "react-router typegen && tsc",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.10.3",
|
"@codemirror/commands": "^6.10.3",
|
||||||
|
|
|
||||||
1
apps/planner/vitest.config.ts
Normal file
1
apps/planner/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -3,7 +3,7 @@ import tseslint from "typescript-eslint";
|
||||||
import prettier from "eslint-config-prettier";
|
import prettier from "eslint-config-prettier";
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ["**/build/", "**/dist/", "**/.react-router/", "**/node_modules/"] },
|
{ ignores: ["**/build/", "**/dist/", "**/.react-router/", "**/.expo/", "**/node_modules/"] },
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@
|
||||||
|
|
||||||
### 1.3 Shared Package Compatibility
|
### 1.3 Shared Package Compatibility
|
||||||
|
|
||||||
- [ ] 1.3.1 Audit `@trails-cool/types` for DOM/Node.js dependencies — confirm it's pure TypeScript interfaces
|
- [x] 1.3.1 Audit `@trails-cool/types` for DOM/Node.js dependencies — confirm it's pure TypeScript interfaces
|
||||||
- [ ] 1.3.2 Refactor `@trails-cool/gpx` to use a platform-agnostic XML parser: use `linkedom` on Node.js and `DOMParser` on React Native/browser
|
- [x] 1.3.2 Refactor `@trails-cool/gpx` to use a platform-agnostic XML parser: use `linkedom` on Node.js and `DOMParser` on React Native/browser
|
||||||
- [ ] 1.3.3 Write unit tests for `parseGpx()` and `generateGpx()` running in a jsdom-free environment to verify no DOM dependency
|
- [x] 1.3.3 Write unit tests for `parseGpx()` and `generateGpx()` running in a jsdom-free environment to verify no DOM dependency
|
||||||
- [ ] 1.3.4 Verify `@trails-cool/i18n` initializes in React Native — add an `initMobile()` export if the current init assumes a browser environment
|
- [x] 1.3.4 Verify `@trails-cool/i18n` initializes in React Native — add an `initMobile()` export if the current init assumes a browser environment
|
||||||
- [ ] 1.3.5 Add `apps/mobile` to Turborepo pipeline (`turbo.json`) for build and typecheck
|
- [x] 1.3.5 Add `apps/mobile` to Turborepo pipeline (`turbo.json`) for build and typecheck
|
||||||
|
|
||||||
### 1.4 Journal Auth (OAuth2 PKCE)
|
### 1.4 Journal Auth (OAuth2 PKCE)
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
|
|
||||||
### 4.1 Unit & Component Tests
|
### 4.1 Unit & Component Tests
|
||||||
|
|
||||||
- [ ] 4.1.1 Set up Jest + jest-expo + React Native Testing Library in `apps/mobile/`
|
- [x] 4.1.1 Set up Jest + jest-expo + React Native Testing Library in `apps/mobile/`
|
||||||
- [ ] 4.1.2 Write unit tests for API client (mocked fetch, auth refresh, error handling)
|
- [ ] 4.1.2 Write unit tests for API client (mocked fetch, auth refresh, error handling)
|
||||||
- [ ] 4.1.3 Write component tests for route list, route detail, and route editor screens
|
- [ ] 4.1.3 Write component tests for route list, route detail, and route editor screens
|
||||||
- [ ] 4.1.4 Write unit tests for offline SQLite storage layer
|
- [ ] 4.1.4 Write unit tests for offline SQLite storage layer
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"dev": "turbo dev",
|
"dev": "turbo dev",
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"test": "vitest run",
|
"test": "turbo test",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^3.25.0"
|
"zod": "^3.25.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,16 @@ import {
|
||||||
TokenExchangeRequestSchema,
|
TokenExchangeRequestSchema,
|
||||||
TokenResponseSchema,
|
TokenResponseSchema,
|
||||||
DeviceSchema,
|
DeviceSchema,
|
||||||
DeviceListResponseSchema,
|
|
||||||
// Routes
|
// Routes
|
||||||
RouteSummarySchema,
|
RouteSummarySchema,
|
||||||
RouteDetailSchema,
|
|
||||||
RouteListResponseSchema,
|
RouteListResponseSchema,
|
||||||
CreateRouteRequestSchema,
|
CreateRouteRequestSchema,
|
||||||
UpdateRouteRequestSchema,
|
UpdateRouteRequestSchema,
|
||||||
ComputeRouteRequestSchema,
|
ComputeRouteRequestSchema,
|
||||||
// Activities
|
// Activities
|
||||||
ActivitySummarySchema,
|
|
||||||
ActivityDetailSchema,
|
|
||||||
ActivityListResponseSchema,
|
|
||||||
CreateActivityRequestSchema,
|
CreateActivityRequestSchema,
|
||||||
// Uploads
|
// Uploads
|
||||||
PresignedUploadRequestSchema,
|
PresignedUploadRequestSchema,
|
||||||
PresignedUploadResponseSchema,
|
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
|
|
||||||
describe("API_VERSION", () => {
|
describe("API_VERSION", () => {
|
||||||
|
|
|
||||||
1
packages/api/vitest.config.ts
Normal file
1
packages/api/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -9,8 +9,16 @@
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"postgres": "catalog:"
|
"postgres": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src"
|
"rootDir": "src",
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/db/vitest.config.ts
Normal file
1
packages/db/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trails-cool/types": "workspace:*"
|
"@trails-cool/types": "workspace:*",
|
||||||
|
"linkedom": "^0.18.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
85
packages/gpx/src/parse-node.test.ts
Normal file
85
packages/gpx/src/parse-node.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* @vitest-environment node
|
||||||
|
*
|
||||||
|
* These tests run WITHOUT jsdom to verify GPX parsing and generation
|
||||||
|
* work in environments without a native DOMParser (Node.js, React Native).
|
||||||
|
* The linkedom fallback must handle all XML parsing.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { parseGpxAsync } from "./parse.ts";
|
||||||
|
import { generateGpx } from "./generate.ts";
|
||||||
|
|
||||||
|
const sampleGpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx version="1.1" creator="test" xmlns="http://www.topografix.com/GPX/1/1">
|
||||||
|
<metadata><name>Test Route</name><desc>A test description</desc></metadata>
|
||||||
|
<wpt lat="52.52" lon="13.405"><name>Berlin</name></wpt>
|
||||||
|
<wpt lat="51.84" lon="12.243"><name>Dessau</name><type>overnight</type></wpt>
|
||||||
|
<wpt lat="48.137" lon="11.576"><name>Munich</name></wpt>
|
||||||
|
<trk>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="52.52" lon="13.405"><ele>34</ele></trkpt>
|
||||||
|
<trkpt lat="51.84" lon="12.243"><ele>80</ele></trkpt>
|
||||||
|
<trkpt lat="48.137" lon="11.576"><ele>519</ele></trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>`;
|
||||||
|
|
||||||
|
describe("parseGpxAsync (node environment, no DOMParser)", () => {
|
||||||
|
it("parses route name and description", async () => {
|
||||||
|
const result = await parseGpxAsync(sampleGpx);
|
||||||
|
expect(result.name).toBe("Test Route");
|
||||||
|
expect(result.description).toBe("A test description");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses waypoints", async () => {
|
||||||
|
const result = await parseGpxAsync(sampleGpx);
|
||||||
|
expect(result.waypoints).toHaveLength(3);
|
||||||
|
expect(result.waypoints[0]).toEqual({ lat: 52.52, lon: 13.405, name: "Berlin" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses isDayBreak from overnight type", async () => {
|
||||||
|
const result = await parseGpxAsync(sampleGpx);
|
||||||
|
expect(result.waypoints[1]!.isDayBreak).toBe(true);
|
||||||
|
expect(result.waypoints[0]!.isDayBreak).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses tracks with elevation", async () => {
|
||||||
|
const result = await parseGpxAsync(sampleGpx);
|
||||||
|
expect(result.tracks).toHaveLength(1);
|
||||||
|
expect(result.tracks[0]).toHaveLength(3);
|
||||||
|
expect(result.tracks[0]![0]!.ele).toBe(34);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes elevation gain and distance", async () => {
|
||||||
|
const result = await parseGpxAsync(sampleGpx);
|
||||||
|
expect(result.elevation.gain).toBe(485);
|
||||||
|
expect(result.elevation.loss).toBe(0);
|
||||||
|
expect(result.distance).toBeGreaterThan(400_000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateGpx + parseGpxAsync round-trip (node environment)", () => {
|
||||||
|
it("round-trips waypoints, tracks, and metadata", async () => {
|
||||||
|
const gpx = generateGpx({
|
||||||
|
name: "Round Trip",
|
||||||
|
description: "Testing",
|
||||||
|
waypoints: [
|
||||||
|
{ lat: 52.52, lon: 13.405, name: "Start" },
|
||||||
|
{ lat: 51.0, lon: 12.0, name: "Camp", isDayBreak: true },
|
||||||
|
{ lat: 48.137, lon: 11.576, name: "End" },
|
||||||
|
],
|
||||||
|
tracks: [[
|
||||||
|
{ lat: 52.52, lon: 13.405, ele: 34 },
|
||||||
|
{ lat: 51.0, lon: 12.0, ele: 200 },
|
||||||
|
{ lat: 48.137, lon: 11.576, ele: 519 },
|
||||||
|
]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = await parseGpxAsync(gpx);
|
||||||
|
expect(parsed.name).toBe("Round Trip");
|
||||||
|
expect(parsed.description).toBe("Testing");
|
||||||
|
expect(parsed.waypoints).toHaveLength(3);
|
||||||
|
expect(parsed.waypoints[1]!.isDayBreak).toBe(true);
|
||||||
|
expect(parsed.tracks[0]).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*
|
||||||
|
* Tests the browser DOMParser path. The linkedom/node path is covered
|
||||||
|
* by parse-node.test.ts.
|
||||||
|
*/
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { parseGpxAsync } from "./parse.ts";
|
import { parseGpxAsync } from "./parse.ts";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ async function getDOMParser(): Promise<typeof DOMParser> {
|
||||||
return _LinkedDOMParser;
|
return _LinkedDOMParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseGpx(xml: string): GpxData {
|
export function parseGpx(xml: string): GpxData {
|
||||||
// Synchronous path for browser
|
// Synchronous path for browser
|
||||||
if (typeof DOMParser !== "undefined") {
|
if (typeof DOMParser !== "undefined") {
|
||||||
return parseGpxWithParser(new DOMParser(), xml);
|
return parseGpxWithParser(new DOMParser(), xml);
|
||||||
|
|
@ -26,7 +26,7 @@ function parseGpx(xml: string): GpxData {
|
||||||
const { DOMParser: LP } = require("linkedom");
|
const { DOMParser: LP } = require("linkedom");
|
||||||
return parseGpxWithParser(new LP() as unknown as DOMParser, xml);
|
return parseGpxWithParser(new LP() as unknown as DOMParser, xml);
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error("DOMParser not available — install linkedom for Node.js");
|
throw new Error("DOMParser not available — install linkedom");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src"
|
"rootDir": "src",
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/gpx/vitest.config.ts
Normal file
1
packages/gpx/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -8,6 +8,11 @@
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
"i18next": ">=26",
|
"i18next": ">=26",
|
||||||
|
|
|
||||||
40
packages/i18n/src/index.test.ts
Normal file
40
packages/i18n/src/index.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* @vitest-environment node
|
||||||
|
*
|
||||||
|
* Tests run in node environment (no browser APIs) to verify
|
||||||
|
* React Native compatibility of initI18nMobile.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { initI18nMobile, i18n, supportedLngs, resources } from "./index.ts";
|
||||||
|
|
||||||
|
describe("initI18nMobile", () => {
|
||||||
|
it("initializes i18next without browser APIs", () => {
|
||||||
|
// Verify no browser globals exist in this environment
|
||||||
|
expect(typeof document).toBe("undefined");
|
||||||
|
expect(typeof localStorage).toBe("undefined");
|
||||||
|
|
||||||
|
initI18nMobile("de-AT");
|
||||||
|
expect(i18n.isInitialized).toBe(true);
|
||||||
|
// Regional variant should match supported "de"
|
||||||
|
expect(i18n.language).toBe("de");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has all translation namespaces loaded", () => {
|
||||||
|
expect(i18n.hasResourceBundle("en", "common")).toBe(true);
|
||||||
|
expect(i18n.hasResourceBundle("de", "common")).toBe(true);
|
||||||
|
expect(i18n.hasResourceBundle("en", "planner")).toBe(true);
|
||||||
|
expect(i18n.hasResourceBundle("de", "journal")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can switch language after init", () => {
|
||||||
|
i18n.changeLanguage("en");
|
||||||
|
expect(i18n.language).toBe("en");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exports supported languages and resources", () => {
|
||||||
|
expect(supportedLngs).toContain("en");
|
||||||
|
expect(supportedLngs).toContain("de");
|
||||||
|
expect(resources.en.common).toBeDefined();
|
||||||
|
expect(resources.de.common).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -53,6 +53,33 @@ export function initI18nClient() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize i18next for React Native.
|
||||||
|
* No browser language detector — pass the device locale directly.
|
||||||
|
* Use `expo-localization` to get the device locale:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { getLocales } from "expo-localization";
|
||||||
|
* initI18nMobile(getLocales()[0]?.languageCode ?? "en");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function initI18nMobile(deviceLanguage?: string) {
|
||||||
|
if (i18n.isInitialized) return;
|
||||||
|
const lng = matchSupportedLng(deviceLanguage);
|
||||||
|
i18n.use(initReactI18next).init({ ...commonOptions, lng });
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSupportedLng(lang?: string): SupportedLng {
|
||||||
|
if (!lang) return "en";
|
||||||
|
const lower = lang.toLowerCase();
|
||||||
|
for (const supported of supportedLngs) {
|
||||||
|
if (lower === supported || lower.startsWith(supported + "-")) {
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect the best supported language from a request's Accept-Language header.
|
* Detect the best supported language from a request's Accept-Language header.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
1
packages/i18n/vitest.config.ts
Normal file
1
packages/i18n/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -6,5 +6,10 @@
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts"
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/map-core/vitest.config.ts
Normal file
1
packages/map-core/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -7,6 +7,11 @@
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trails-cool/map-core": "workspace:*"
|
"@trails-cool/map-core": "workspace:*"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
1
packages/map/vitest.config.ts
Normal file
1
packages/map/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -3,5 +3,10 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts"
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import type { Waypoint, Route } from "./index.ts";
|
import type { Waypoint } from "./index.ts";
|
||||||
|
|
||||||
describe("types", () => {
|
describe("types", () => {
|
||||||
it("Waypoint type accepts valid data", () => {
|
it("Waypoint type accepts valid data", () => {
|
||||||
|
|
|
||||||
1
packages/types/vitest.config.ts
Normal file
1
packages/types/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
|
|
@ -7,5 +7,10 @@
|
||||||
"./styles.css": "./src/styles.css"
|
"./styles.css": "./src/styles.css"
|
||||||
},
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts"
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/ui/vitest.config.ts
Normal file
1
packages/ui/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "../../vitest.shared.ts";
|
||||||
1514
pnpm-lock.yaml
generated
1514
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,3 +21,4 @@ catalog:
|
||||||
"@sentry/node": ^10.48.0
|
"@sentry/node": ^10.48.0
|
||||||
"@sentry/react": ^10.48.0
|
"@sentry/react": ^10.48.0
|
||||||
postgres: ^3.4.9
|
postgres: ^3.4.9
|
||||||
|
"@types/node": ^22.0.0
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^build"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default defineConfig({
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
include: ["packages/*/src/**/*.test.{ts,tsx}", "apps/*/app/**/*.test.{ts,tsx}"],
|
include: ["packages/*/src/**/*.test.{ts,tsx}", "apps/*/app/**/*.test.{ts,tsx}"],
|
||||||
exclude: ["**/e2e/**"],
|
exclude: ["**/e2e/**", "apps/mobile/**"],
|
||||||
setupFiles: ["./vitest.setup.ts"],
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
8
vitest.shared.ts
Normal file
8
vitest.shared.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
passWithNoTests: true,
|
||||||
|
include: ["src/**/*.test.{ts,tsx}", "app/**/*.test.{ts,tsx}"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue