@xmachines/shared 1.0.0-beta.6 → 1.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Centralized config enabling composite build system with project references.
6
6
 
7
7
  ## Overview
8
8
 
9
- `@xmachines/shared` provides shared configuration files for all XMachines packages, establishing consistent TypeScript compilation, linting rules, and code formatting across the monorepo.
9
+ `@xmachines/shared` provides shared configuration files for all XMachines packages, establishing consistent TypeScript compilation, linting rules, formatting, and Vitest setup across the monorepo.
10
10
 
11
11
  **Enhancement:** Added TypeScript composite build system with project references enabling correct build order, incremental compilation, and IDE source navigation via declaration maps.
12
12
 
@@ -21,6 +21,10 @@ Configuration files in `config/` directory:
21
21
  - **`config/tsconfig.json`** - TypeScript compiler settings with composite project support
22
22
  - **`config/oxlint.json`** - Linting rules and plugins
23
23
  - **`config/oxfmt.json`** - Code formatting preferences
24
+ - **`config/vitest.ts`** - Shared Vitest config helper (`defineXmVitestConfig`)
25
+ - **`config/vitest.setup.ts`** - Shared cross-runtime Vitest setup (polyfills, jest-dom matchers)
26
+ - **`config/vitest.node.setup.ts`** - Node-only Vitest setup (Node runtime/version preflight)
27
+ - **`config/vite-aliases.ts`** - Workspace alias mapping for `@xmachines/*`
24
28
 
25
29
  ## Usage
26
30
 
@@ -74,6 +78,93 @@ Configuration files in `config/` directory:
74
78
  }
75
79
  ```
76
80
 
81
+ ### Vite Alias Configuration
82
+
83
+ Use the shared alias helper in Vite and Vitest configs so workspace packages resolve to source files without requiring `dist/` builds:
84
+
85
+ ```ts
86
+ import { defineConfig } from "vite";
87
+ import { xmAliases } from "@xmachines/shared/vite-aliases";
88
+
89
+ export default defineConfig({
90
+ resolve: {
91
+ alias: xmAliases(import.meta.url),
92
+ },
93
+ });
94
+ ```
95
+
96
+ This is especially important after `npm run clean`, where package `exports` targeting `dist/` are temporarily unavailable until rebuild.
97
+
98
+ ### Vitest Configuration
99
+
100
+ Use the shared helper so package configs inherit common aliasing and runtime setup:
101
+
102
+ ```ts
103
+ import { defineXmVitestConfig } from "@xmachines/shared/vitest";
104
+
105
+ export default defineXmVitestConfig(import.meta.url, {
106
+ test: {
107
+ environment: "node",
108
+ include: ["test/**/*.test.ts"],
109
+ exclude: ["node_modules/**"],
110
+ },
111
+ });
112
+ ```
113
+
114
+ **Browser project example:**
115
+
116
+ ```ts
117
+ import { defineXmVitestConfig } from "@xmachines/shared/vitest";
118
+ import { playwright } from "@vitest/browser-playwright";
119
+
120
+ export default defineXmVitestConfig(import.meta.url, {
121
+ test: {
122
+ name: "my-demo-browser",
123
+ browser: {
124
+ enabled: true,
125
+ provider: playwright(),
126
+ headless: true,
127
+ instances: [{ browser: "chromium" }],
128
+ },
129
+ include: ["test/browser/**/*.browser.test.ts"],
130
+ exclude: ["node_modules/**"],
131
+ },
132
+ });
133
+ ```
134
+
135
+ What the shared Vitest layer provides automatically:
136
+
137
+ - `resolve.alias` for all `@xmachines/*` packages via `xmAliases(...)`
138
+ - Shared setup injection (`config/vitest.setup.ts`) unless already present
139
+ - Node setup injection (`config/vitest.node.setup.ts`) for non-browser projects
140
+ - Runtime preflight:
141
+ - Node.js runtime/version check (`>=22`) in node setup
142
+ - `urlpattern-polyfill` load in shared setup
143
+ - `URLPattern` availability assertion in shared setup
144
+ - DOM matcher extension via `@testing-library/jest-dom/vitest`
145
+
146
+ ### Vitest Setup Injection Rules
147
+
148
+ `defineXmVitestConfig(...)` applies setup files predictably:
149
+
150
+ 1. It normalizes `test.setupFiles` into an array.
151
+ 2. It injects `config/vitest.setup.ts` if missing.
152
+ 3. It injects `config/vitest.node.setup.ts` only when `test.browser.enabled !== true`.
153
+ 4. It avoids duplicates by checking filenames (suffix match).
154
+ 5. Injected setup files run before user-provided setup files.
155
+
156
+ This gives a safe default for Node package tests while keeping browser projects compatible.
157
+
158
+ ### Vitest Exports
159
+
160
+ `@xmachines/shared` exports these Vitest entry points:
161
+
162
+ - `@xmachines/shared/vitest` → `config/vitest.ts`
163
+ - `@xmachines/shared/vitest-setup` → `config/vitest.setup.ts`
164
+ - `@xmachines/shared/vitest-node-setup` → `config/vitest.node.setup.ts`
165
+
166
+ Use `@xmachines/shared/vitest` for package config files; direct setup exports are for advanced customization only.
167
+
77
168
  ## Configuration Details
78
169
 
79
170
  ### TypeScript (`config/tsconfig.json`)
@@ -107,6 +198,48 @@ import { foo } from "./bar"; // ❌ Wrong
107
198
  - **Style:** Double quotes, semicolons, trailing commas
108
199
  - **Final Newline:** Always insert
109
200
 
201
+ ### Vitest (`config/vitest.ts`, `config/vitest.setup.ts`, `config/vitest.node.setup.ts`)
202
+
203
+ - **Helper API:** `defineXmVitestConfig(importMetaUrl, overrides)` merges shared defaults with package-specific Vitest config
204
+ - **Auto-aliasing:** `resolve.alias` is wired to `xmAliases(importMetaUrl)` so workspace packages resolve to source
205
+ - **Cross-runtime setup (`config/vitest.setup.ts`):**
206
+ - loads `urlpattern-polyfill`
207
+ - extends `expect` with `@testing-library/jest-dom/vitest`
208
+ - asserts `URLPattern` availability
209
+ - **Node-only setup (`config/vitest.node.setup.ts`):**
210
+ - asserts Node runtime (`process.release.name === "node"`)
211
+ - enforces Node version `>=22`
212
+ - **Browser behavior:** Browser projects do not receive node-only setup injection
213
+ - **Override model:** Package-level config still controls test environment, includes/excludes, plugins, coverage, and browser provider
214
+
215
+ ### Vite Aliases (`config/vite-aliases.ts`)
216
+
217
+ - **Helper API:** `xmAliases(importMetaUrl)` returns a complete `resolve.alias` map for all `@xmachines/*` workspace packages
218
+ - **Root discovery:** Walks upward from `import.meta.url` until it finds the workspace root (`package.json` with `workspaces`)
219
+ - **Source-first resolution:** Maps package imports to `packages/*/src/index.ts` for fast local dev without prebuild
220
+ - **Extra aliases:** Includes `@xmachines/play-router-shared` and `@xmachines/play-router-shared/index.css` for demo shared assets
221
+ - **Primary use cases:** `vite.config.ts`, `vitest` browser configs, and any local tooling relying on Vite resolver semantics
222
+
223
+ ### Vite Alias Troubleshooting
224
+
225
+ - **`[xmAliases] Could not find workspace root`**
226
+ - Ensure `xmAliases` receives `import.meta.url` from the calling config file.
227
+ - Ensure the config file lives somewhere under the monorepo root.
228
+ - **Workspace package import resolves to missing `dist/` files**
229
+ - Ensure config includes `resolve.alias: xmAliases(import.meta.url)`.
230
+ - Re-run after cleaning caches when lockfile/deps changed.
231
+
232
+ ### Vitest Troubleshooting
233
+
234
+ - **`URLPattern is unavailable after setup`**
235
+ - Ensure `test.setupFiles` is not replacing shared setup unexpectedly.
236
+ - Ensure config is created via `defineXmVitestConfig(...)`.
237
+ - **`Node runtime is required for this Vitest configuration`**
238
+ - This indicates node-only setup loaded in a browser project.
239
+ - Set `test.browser.enabled: true` in that project config.
240
+ - **Workspace import resolution failures (e.g. `@xmachines/play-signals`)**
241
+ - Confirm config uses `defineXmVitestConfig(import.meta.url, ...)` so aliases are injected.
242
+
110
243
  ## TypeScript Composite Build System
111
244
 
112
245
  This package introduced TypeScript project references for proper build-order management in the monorepo.
@@ -228,11 +361,9 @@ To add a new shared configuration (e.g., for Jest, Vitest, etc.):
228
361
  3. Document in this README with usage examples
229
362
  4. Note whether the tool supports package exports or requires relative paths
230
363
 
231
- ## Links
232
-
233
- - [XMachines Monorepo](../../README.md)
234
- - Agent Guidelines: `AGENTS.md`
235
-
236
364
  ## License
237
365
 
238
- MIT
366
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
367
+
368
+ This work is licensed under the terms of the MIT license.
369
+ For a copy, see <https://opensource.org/licenses/MIT>.
@@ -11,7 +11,7 @@
11
11
  "rules": {
12
12
  "unicorn/filename-case": "off",
13
13
  "import/no-cycle": "error",
14
- "typescript/no-explicit-any": "warn",
14
+ "typescript/no-explicit-any": "error",
15
15
  "typescript/no-unused-vars": [
16
16
  "error",
17
17
  {
@@ -19,9 +19,6 @@ import { fileURLToPath } from "url";
19
19
  *
20
20
  * @param importMetaUrl - Pass `import.meta.url` from the calling config file.
21
21
  * Used to locate the workspace root (the directory containing `packages/`).
22
- * @internal
23
- * @hidden
24
- * @ignore
25
22
  */
26
23
  export function xmAliases(importMetaUrl: string): Record<string, string> {
27
24
  const configDir = fileURLToPath(new URL(".", importMetaUrl));
@@ -0,0 +1,15 @@
1
+ const isNodeRuntime =
2
+ typeof process !== "undefined" &&
3
+ process.release?.name === "node" &&
4
+ typeof process.versions?.node === "string";
5
+
6
+ if (!isNodeRuntime) {
7
+ throw new Error("Node runtime is required for this Vitest configuration");
8
+ }
9
+
10
+ const [nodeMajorRaw] = process.versions.node.split(".");
11
+ const nodeMajor = Number.parseInt(nodeMajorRaw ?? "0", 10);
12
+
13
+ if (!Number.isFinite(nodeMajor) || nodeMajor < 22) {
14
+ throw new Error(`Vitest runtime requires Node.js >=22.0.0, received ${process.versions.node}`);
15
+ }
@@ -4,41 +4,10 @@
4
4
  * @internal
5
5
  */
6
6
  // eslint-disable-next-line import/no-unassigned-import
7
- import "@testing-library/jest-dom/vitest";
8
- // eslint-disable-next-line import/no-unassigned-import
9
7
  import "urlpattern-polyfill";
10
- import { afterAll } from "vitest";
11
-
12
- if (!(globalThis as { URLPattern?: unknown }).URLPattern) {
13
- throw new Error("URLPattern polyfill failed to load in Vitest setup");
14
- }
15
-
16
- let originalEmit: typeof process.emit | undefined;
17
-
18
- if (typeof process !== "undefined") {
19
- type ProcessWithEmit = NodeJS.Process & {
20
- emit: (event: string, error: unknown, ...args: unknown[]) => boolean;
21
- };
22
-
23
- originalEmit = process.emit.bind(process);
24
- const processWithEmit = process as ProcessWithEmit;
25
-
26
- processWithEmit.emit = function (event: string, error: unknown, ...args: unknown[]) {
27
- if (
28
- event === "uncaughtException" &&
29
- error instanceof Error &&
30
- (error.message.includes("Unable to evaluate guard") ||
31
- error.message.includes("Cannot read properties of null"))
32
- ) {
33
- return false;
34
- }
8
+ // eslint-disable-next-line import/no-unassigned-import
9
+ import "@testing-library/jest-dom/vitest";
35
10
 
36
- return originalEmit!(event, error, ...args);
37
- };
11
+ if (typeof (globalThis as { URLPattern?: unknown }).URLPattern !== "function") {
12
+ throw new Error("URLPattern is unavailable after setup");
38
13
  }
39
-
40
- afterAll(() => {
41
- if (typeof process !== "undefined" && originalEmit) {
42
- process.emit = originalEmit;
43
- }
44
- });
@@ -0,0 +1,87 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, resolve } from "node:path";
3
+ import { defineConfig, mergeConfig } from "vitest/config";
4
+ import { xmAliases } from "@xmachines/shared/vite-aliases";
5
+
6
+ const configDir = dirname(fileURLToPath(import.meta.url));
7
+ const nodeSetupFile = resolve(configDir, "vitest.node.setup.ts");
8
+ const setupFile = resolve(configDir, "vitest.setup.ts");
9
+
10
+ /**
11
+ * Normalize Vitest `test.setupFiles` into a string array.
12
+ */
13
+ function normalizeSetupFiles(setupFiles: unknown): string[] {
14
+ if (!setupFiles) return [];
15
+ if (Array.isArray(setupFiles)) {
16
+ return setupFiles.filter((entry): entry is string => typeof entry === "string");
17
+ }
18
+ return typeof setupFiles === "string" ? [setupFiles] : [];
19
+ }
20
+
21
+ /**
22
+ * True when Vitest browser mode is explicitly enabled.
23
+ */
24
+ function isBrowserEnabled(browserConfig: unknown): boolean {
25
+ if (!browserConfig || typeof browserConfig !== "object") return false;
26
+ return (browserConfig as { enabled?: unknown }).enabled === true;
27
+ }
28
+
29
+ /**
30
+ * Checks whether setup files already include a file by suffix.
31
+ *
32
+ * This supports both absolute and relative setup file entries and normalizes
33
+ * Windows separators for cross-platform matching.
34
+ */
35
+ function hasSetupFile(setupFiles: string[], fileName: string): boolean {
36
+ const normalizedSuffix = `/${fileName}`;
37
+ return setupFiles.some((entry) => entry.replaceAll("\\", "/").endsWith(normalizedSuffix));
38
+ }
39
+
40
+ /**
41
+ * Create a Vitest config with XMachines workspace defaults.
42
+ *
43
+ * What this applies automatically:
44
+ * - `resolve.alias` from `xmAliases(importMetaUrl)` so `@xmachines/*` imports
45
+ * resolve to source in local/dev test runs.
46
+ * - `config/vitest.setup.ts` when missing.
47
+ * - `config/vitest.node.setup.ts` for non-browser projects when missing.
48
+ *
49
+ * Setup injection is additive and preserves caller-provided ordering after
50
+ * required shared setup files.
51
+ *
52
+ * @param importMetaUrl Pass `import.meta.url` from the calling config file.
53
+ * @param overrides Package/project-specific Vitest config overrides.
54
+ * @returns Merged Vitest configuration object.
55
+ */
56
+ export function defineXmVitestConfig(importMetaUrl: string, overrides: Record<string, unknown>) {
57
+ const merged = mergeConfig(
58
+ defineConfig({
59
+ resolve: {
60
+ alias: xmAliases(importMetaUrl),
61
+ },
62
+ }),
63
+ defineConfig(overrides),
64
+ );
65
+
66
+ const testConfig = merged.test ?? {};
67
+ const setupFiles = normalizeSetupFiles(testConfig.setupFiles);
68
+ const browserEnabled = isBrowserEnabled(testConfig.browser);
69
+ const hasNodeSetup = hasSetupFile(setupFiles, "vitest.node.setup.ts");
70
+ const hasSharedSetup = hasSetupFile(setupFiles, "vitest.setup.ts");
71
+ const injectedSetupFiles: string[] = [];
72
+
73
+ if (!browserEnabled && !hasNodeSetup) {
74
+ injectedSetupFiles.push(nodeSetupFile);
75
+ }
76
+ if (!hasSharedSetup) {
77
+ injectedSetupFiles.push(setupFile);
78
+ }
79
+
80
+ return {
81
+ ...merged,
82
+ test: {
83
+ ...testConfig,
84
+ setupFiles: [...injectedSetupFiles, ...setupFiles],
85
+ },
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/shared",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "Shared configurations for XMachines packages",
5
5
  "keywords": [
6
6
  "config",
@@ -25,23 +25,32 @@
25
25
  "./oxlint": "./config/oxlint.json",
26
26
  "./oxfmt": "./config/oxfmt.json",
27
27
  "./vite-aliases": "./config/vite-aliases.ts",
28
+ "./vitest": "./config/vitest.ts",
29
+ "./vitest-node-setup": "./config/vitest.node.setup.ts",
28
30
  "./vitest-setup": "./config/vitest.setup.ts",
29
31
  "./config/tsconfig.json": "./config/tsconfig.json",
30
32
  "./config/oxlint.json": "./config/oxlint.json",
31
33
  "./config/oxfmt.json": "./config/oxfmt.json",
34
+ "./config/vitest.ts": "./config/vitest.ts",
35
+ "./config/vitest.node.setup.ts": "./config/vitest.node.setup.ts",
32
36
  "./config/vitest.setup.ts": "./config/vitest.setup.ts"
33
37
  },
34
38
  "publishConfig": {
35
39
  "access": "public"
36
40
  },
37
41
  "scripts": {
42
+ "clean": "rm -rf dist coverage *.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
38
43
  "lint": "oxlint .",
39
44
  "lint:fix": "oxlint --fix .",
40
45
  "format": "oxfmt .",
41
46
  "format:check": "oxfmt --check .",
42
- "test": "echo \"Error: no test specified\" && exit 1"
47
+ "test": "vitest"
43
48
  },
44
49
  "dependencies": {
45
- "@testing-library/jest-dom": "^6.9.1"
50
+ "@testing-library/jest-dom": "^6.9.1",
51
+ "urlpattern-polyfill": "^10.1.0"
52
+ },
53
+ "devDependencies": {
54
+ "vitest": "^4.1.0"
46
55
  }
47
56
  }