@canonical/vitest-config-react 0.29.0

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 ADDED
@@ -0,0 +1,95 @@
1
+ # Canonical Vitest Configuration (React)
2
+
3
+ This package provides a reusable configuration factory for the Vitest `test`
4
+ block of Canonical's React packages. It replaces the near-identical `test`
5
+ blocks that were previously copy-pasted into every package's `vite.config.ts`.
6
+
7
+ Every React vitest posture in the monorepo — jsdom component tests, the
8
+ client/SSR project split, coverage-gated packages, and node-only packages — is
9
+ a point in one option space, so this is a single factory rather than a set of
10
+ per-shape variants.
11
+
12
+ ## Getting started
13
+
14
+ 1. Add the package as a dev dependency: `bun add -d @canonical/vitest-config-react`.
15
+ 2. In your `vite.config.ts`, import the factory and spread its result into
16
+ `test`:
17
+
18
+ ```typescript
19
+ import react from "@vitejs/plugin-react";
20
+ import { reactTestConfig } from "@canonical/vitest-config-react";
21
+ import { defineConfig } from "vitest/config";
22
+
23
+ export default defineConfig({
24
+ plugins: [react()],
25
+ resolve: { tsconfigPaths: true },
26
+ build: { sourcemap: true },
27
+ test: reactTestConfig({
28
+ glob: "tests",
29
+ setupFiles: ["./vitest.setup.ts"],
30
+ }),
31
+ });
32
+ ```
33
+
34
+ The factory owns only the `test` block. `plugins`, `resolve` and `build` stay
35
+ in each package's config, because those legitimately differ (for example, some
36
+ packages run in `jsdom` while framework-agnostic/SSR packages run in `node`).
37
+
38
+ ## Options
39
+
40
+ | Option | Type | Default | Notes |
41
+ | ------------- | --------------------------------- | --------- | ---------------------------------------------------------------------------------------------- |
42
+ | `glob` | `"test" \| "tests"`, or an array of those | — | **Required.** The test-file suffix convention(s). See the caveat below. |
43
+ | `environment` | `"jsdom" \| "node"` | `"jsdom"` | `"node"` for framework-agnostic / SSR-only packages. |
44
+ | `ssr` | `boolean` | `false` | Adds a second `node` project running `**/*.ssr.<glob>.tsx` (the client project excludes them). |
45
+ | `coverage` | `boolean \| { … }` | `false` | `true` enables v8 coverage with 100% thresholds; pass an object to override. |
46
+ | `setupFiles` | `string[]` | `[]` | e.g. `["./vitest.setup.ts"]`; omitted when empty. |
47
+ | `plugins` | `Plugin[]` | — | Attached to each project (Vitest resolves plugins per project). |
48
+
49
+ ### Coverage
50
+
51
+ Coverage is **off by default**, preserving the behaviour of packages that do
52
+ not gate coverage today. When enabled, thresholds default to
53
+ `100/100/100/100`, following the storybook-config convention of "sensible
54
+ defaults you pass through":
55
+
56
+ ```typescript
57
+ // 100% thresholds
58
+ reactTestConfig({ glob: "test", coverage: true });
59
+
60
+ // keep 100% for everything except lines
61
+ reactTestConfig({ glob: "test", coverage: { thresholds: { lines: 80 } } });
62
+
63
+ // runtime-only include (SSR adapters)
64
+ reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**/*.ts"] } });
65
+ ```
66
+
67
+ ## Caveat: `glob` is load-bearing
68
+
69
+ `glob` has no default because `.test.` and `.tests.` are **not**
70
+ interchangeable: choosing the wrong convention silently matches zero files — a
71
+ green run that ran nothing. Always pass the convention(s) the package's test
72
+ files actually use.
73
+
74
+ ### Interim: running both conventions
75
+
76
+ A package whose test files are split between `.test.` and `.tests.` (for
77
+ example `react-ds-global`, whose `_work_in_progress` scaffolds use `.test.`
78
+ while the promoted components use `.tests.`) can pass both:
79
+
80
+ ```typescript
81
+ reactTestConfig({ glob: ["test", "tests"], ssr: true });
82
+ ```
83
+
84
+ Both conventions are then matched by the client project and, with `ssr: true`,
85
+ by the SSR project (`**/*.ssr.test.tsx` and `**/*.ssr.tests.tsx`); coverage
86
+ excludes test files of both conventions. This is an **interim** escape hatch:
87
+ once the repo-wide test-file naming unification lands, packages should return
88
+ to a single convention and the array form should disappear.
89
+
90
+ ## Roadmap: browser mode
91
+
92
+ `jsdom` is the interim environment. The forward path is Vitest browser mode
93
+ with Playwright (see the `svelte-ds-global` migration). This factory will gain
94
+ a `browser` option so each package can flip to browser mode without rewriting
95
+ its config — only its test files.
package/biome.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": ["@canonical/biome-config"],
3
+ "files": {
4
+ "includes": ["src", "*.json"]
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ export { reactTestConfig } from "./reactTestConfig.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,99 @@
1
+ /** Standard coverage excludes shared by every coverage-gated package. */
2
+ const BASE_COVERAGE_EXCLUDE = [
3
+ "**/index.ts",
4
+ "**/*.d.ts",
5
+ "**/types.ts",
6
+ ];
7
+ const FULL_THRESHOLDS = {
8
+ branches: 100,
9
+ functions: 100,
10
+ lines: 100,
11
+ statements: 100,
12
+ };
13
+ /**
14
+ * Build the v8 coverage block. Test files are always excluded (both `.test.`
15
+ * and `.tests.` plus their `.ssr.` variants) so coverage measures runtime code
16
+ * only. Thresholds default to 100% and are overridable per the storybook-config
17
+ * convention of "sensible defaults you pass through".
18
+ */
19
+ const buildCoverage = (coverage, globs) => {
20
+ const overrides = coverage === true ? {} : coverage;
21
+ const testExclude = globs.flatMap((glob) => [
22
+ `**/*.${glob}.ts`,
23
+ `**/*.${glob}.tsx`,
24
+ `**/*.ssr.${glob}.ts`,
25
+ `**/*.ssr.${glob}.tsx`,
26
+ ]);
27
+ return {
28
+ provider: "v8",
29
+ include: overrides.include ?? ["src/**/*.{ts,tsx}"],
30
+ exclude: [
31
+ ...BASE_COVERAGE_EXCLUDE,
32
+ ...testExclude,
33
+ ...(overrides.exclude ?? []),
34
+ ],
35
+ thresholds: { ...FULL_THRESHOLDS, ...overrides.thresholds },
36
+ };
37
+ };
38
+ /**
39
+ * Canonical's standard Vitest `test` configuration for React packages.
40
+ *
41
+ * Returns the `test` block to spread into `defineConfig` from `vitest/config`.
42
+ * Every distinct React vitest posture in the monorepo is a point in this
43
+ * option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
44
+ * there is a single factory rather than per-shape variants.
45
+ *
46
+ * `import { reactTestConfig } from "@canonical/vitest-config-react";`
47
+ *
48
+ * @example jsdom component package (ds-app family)
49
+ * ```ts
50
+ * test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
51
+ * ```
52
+ * @example client + ssr split with 100% coverage (hooks, router)
53
+ * ```ts
54
+ * test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
55
+ * ```
56
+ * @example node-only adapter with overridden coverage include
57
+ * ```ts
58
+ * test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
59
+ * ```
60
+ */
61
+ export const reactTestConfig = ({ glob, environment = "jsdom", ssr = false, coverage = false, setupFiles, plugins, }) => {
62
+ const hasSetup = setupFiles !== undefined && setupFiles.length > 0;
63
+ // Normalise to an array: a single convention is the common case, the array
64
+ // form covers packages whose files are split between `.test.` and `.tests.`.
65
+ const globs = Array.isArray(glob) ? glob : [glob];
66
+ // The client project: DOM (or node) tests, excluding the `.ssr.` variants
67
+ // when an ssr project will pick them up.
68
+ const clientTest = {
69
+ name: "client",
70
+ environment,
71
+ globals: true,
72
+ ...(hasSetup ? { setupFiles } : {}),
73
+ include: globs.flatMap((g) => [`src/**/*.${g}.ts`, `src/**/*.${g}.tsx`]),
74
+ ...(ssr ? { exclude: globs.map((g) => `src/**/*.ssr.${g}.tsx`) } : {}),
75
+ };
76
+ const coverageBlock = coverage === false ? {} : { coverage: buildCoverage(coverage, globs) };
77
+ // No split: return a flat single-project test block (the common case).
78
+ if (!ssr) {
79
+ // The client project's identity (`name`) is only meaningful in a split, so
80
+ // a flat config omits it to match the hand-written single-project shape.
81
+ const { name: _name, ...flat } = clientTest;
82
+ return { ...flat, ...coverageBlock };
83
+ }
84
+ // Split: a jsdom/node client project plus a node ssr project that runs only
85
+ // the `.ssr.` files.
86
+ const ssrTest = {
87
+ name: "ssr",
88
+ environment: "node",
89
+ include: globs.map((g) => `src/**/*.ssr.${g}.tsx`),
90
+ };
91
+ return {
92
+ ...coverageBlock,
93
+ projects: [
94
+ { ...(plugins ? { plugins } : {}), test: clientTest },
95
+ { ...(plugins ? { plugins } : {}), test: ssrTest },
96
+ ],
97
+ };
98
+ };
99
+ //# sourceMappingURL=reactTestConfig.js.map
@@ -0,0 +1,3 @@
1
+ export type { CoverageOption, ReactTestConfigOptions, TestGlob, } from "./reactTestConfig.js";
2
+ export { reactTestConfig } from "./reactTestConfig.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,89 @@
1
+ import type { PluginOption } from "vite";
2
+ import type { ViteUserConfig } from "vitest/config";
3
+ /** The `test` block of a Vitest config, as consumed by `defineConfig({ test })`. */
4
+ type TestConfig = NonNullable<ViteUserConfig["test"]>;
5
+ /**
6
+ * The two test-file glob conventions in use across the React packages. This is
7
+ * a REQUIRED option with no default: `.test.` and `.tests.` are load-bearing,
8
+ * and a wrong choice silently matches zero files — a green run that ran
9
+ * nothing. Packages whose files are split between the two conventions pass
10
+ * both as an array until the repo-wide naming unification lands. See
11
+ * pragma-adrs (Track: unified config).
12
+ */
13
+ export type TestGlob = "test" | "tests";
14
+ /** Coverage options. `true` enables v8 coverage with 100% thresholds. */
15
+ export type CoverageOption = boolean | {
16
+ /** Threshold overrides; each defaults to 100 when coverage is enabled. */
17
+ thresholds?: Partial<Record<"branches" | "functions" | "lines" | "statements", number>>;
18
+ /** Extra `coverage.include` globs (defaults to `src/**` for the runtime). */
19
+ include?: string[];
20
+ /** Extra `coverage.exclude` globs, merged after the standard excludes. */
21
+ exclude?: string[];
22
+ };
23
+ export interface ReactTestConfigOptions {
24
+ /**
25
+ * Test-file glob convention. REQUIRED — no default; see {@link TestGlob}.
26
+ *
27
+ * Pass an array to run BOTH conventions (e.g. `["test", "tests"]`) in a
28
+ * package whose files are mid-migration between the two. This is an interim
29
+ * escape hatch until the repo-wide naming unification lands — see the README.
30
+ */
31
+ glob: TestGlob | TestGlob[];
32
+ /**
33
+ * Test environment. `"jsdom"` (default) for DOM component tests, `"node"`
34
+ * for framework-agnostic / SSR-only packages.
35
+ *
36
+ * @note jsdom is the interim environment. The forward path is Vitest browser
37
+ * mode with Playwright (see the svelte-ds-global migration); this factory
38
+ * will gain a `browser` option to drive that per-package flip without a
39
+ * config rewrite.
40
+ */
41
+ environment?: "jsdom" | "node";
42
+ /**
43
+ * When true, adds a second `node`-environment project that runs
44
+ * `**\/*.ssr.<glob>.tsx` files (client project excludes them). Mirrors the
45
+ * client/ssr split used by ds-global, hooks, head, router and tokens.
46
+ */
47
+ ssr?: boolean;
48
+ /**
49
+ * v8 coverage. `false` (default) attaches no coverage block — preserving the
50
+ * behaviour of packages that do not currently gate coverage. `true` enables
51
+ * coverage with 100/100/100/100 thresholds. Pass an object to override
52
+ * thresholds/include/exclude while keeping the 100% defaults for any omitted
53
+ * threshold.
54
+ */
55
+ coverage?: CoverageOption;
56
+ /** Setup files (e.g. `["./vitest.setup.ts"]`); omitted when empty. */
57
+ setupFiles?: string[];
58
+ /**
59
+ * Vite plugins to attach to each project. Vitest resolves plugins per
60
+ * project, so packages with a split pass their `[react()]` here.
61
+ */
62
+ plugins?: any[] | PluginOption[];
63
+ }
64
+ /**
65
+ * Canonical's standard Vitest `test` configuration for React packages.
66
+ *
67
+ * Returns the `test` block to spread into `defineConfig` from `vitest/config`.
68
+ * Every distinct React vitest posture in the monorepo is a point in this
69
+ * option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
70
+ * there is a single factory rather than per-shape variants.
71
+ *
72
+ * `import { reactTestConfig } from "@canonical/vitest-config-react";`
73
+ *
74
+ * @example jsdom component package (ds-app family)
75
+ * ```ts
76
+ * test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
77
+ * ```
78
+ * @example client + ssr split with 100% coverage (hooks, router)
79
+ * ```ts
80
+ * test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
81
+ * ```
82
+ * @example node-only adapter with overridden coverage include
83
+ * ```ts
84
+ * test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
85
+ * ```
86
+ */
87
+ export declare const reactTestConfig: ({ glob, environment, ssr, coverage, setupFiles, plugins, }: ReactTestConfigOptions) => TestConfig;
88
+ export {};
89
+ //# sourceMappingURL=reactTestConfig.d.ts.map
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@canonical/vitest-config-react",
3
+ "version": "0.29.0",
4
+ "type": "module",
5
+ "description": "Canonical's standard Vitest configuration factory for React packages",
6
+ "main": "dist/esm/index.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types/index.d.ts",
11
+ "default": "./dist/esm/index.js"
12
+ }
13
+ },
14
+ "author": {
15
+ "email": "webteam@canonical.com",
16
+ "name": "Canonical Webteam"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/canonical/pragma"
21
+ },
22
+ "license": "LGPL-3.0",
23
+ "bugs": {
24
+ "url": "https://github.com/canonical/pragma/issues"
25
+ },
26
+ "homepage": "https://github.com/canonical/pragma#readme",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json",
32
+ "build:all": "tsc -p tsconfig.build.json",
33
+ "check": "bun run check:biome",
34
+ "check:fix": "bun run check:biome:fix && bun run check:ts",
35
+ "check:biome": "biome check",
36
+ "check:biome:fix": "biome check --write",
37
+ "check:ts": "tsc --noEmit"
38
+ },
39
+ "peerDependencies": {
40
+ "typescript": "^5.9.3",
41
+ "vite": "^8.0.1",
42
+ "vitest": "^4.0.18"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "2.4.9",
46
+ "@canonical/biome-config": "^0.29.0",
47
+ "@canonical/typescript-config": "^0.29.0",
48
+ "@types/node": "^24.12.0",
49
+ "typescript": "^5.9.3",
50
+ "vite": "^8.0.1",
51
+ "vitest": "^4.0.18"
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type {
2
+ CoverageOption,
3
+ ReactTestConfigOptions,
4
+ TestGlob,
5
+ } from "./reactTestConfig.js";
6
+ export { reactTestConfig } from "./reactTestConfig.js";
@@ -0,0 +1,197 @@
1
+ import type { PluginOption } from "vite";
2
+ import type { ViteUserConfig } from "vitest/config";
3
+
4
+ /** The `test` block of a Vitest config, as consumed by `defineConfig({ test })`. */
5
+ type TestConfig = NonNullable<ViteUserConfig["test"]>;
6
+
7
+ /**
8
+ * Vitest's coverage options. Derived from the public `test` type rather than
9
+ * importing the internal `CoverageV8Options` name, which `vitest/config` does
10
+ * not re-export.
11
+ */
12
+ type CoverageConfig = NonNullable<TestConfig["coverage"]>;
13
+
14
+ /**
15
+ * The two test-file glob conventions in use across the React packages. This is
16
+ * a REQUIRED option with no default: `.test.` and `.tests.` are load-bearing,
17
+ * and a wrong choice silently matches zero files — a green run that ran
18
+ * nothing. Packages whose files are split between the two conventions pass
19
+ * both as an array until the repo-wide naming unification lands. See
20
+ * pragma-adrs (Track: unified config).
21
+ */
22
+ export type TestGlob = "test" | "tests";
23
+
24
+ /** Coverage options. `true` enables v8 coverage with 100% thresholds. */
25
+ export type CoverageOption =
26
+ | boolean
27
+ | {
28
+ /** Threshold overrides; each defaults to 100 when coverage is enabled. */
29
+ thresholds?: Partial<
30
+ Record<"branches" | "functions" | "lines" | "statements", number>
31
+ >;
32
+ /** Extra `coverage.include` globs (defaults to `src/**` for the runtime). */
33
+ include?: string[];
34
+ /** Extra `coverage.exclude` globs, merged after the standard excludes. */
35
+ exclude?: string[];
36
+ };
37
+
38
+ export interface ReactTestConfigOptions {
39
+ /**
40
+ * Test-file glob convention. REQUIRED — no default; see {@link TestGlob}.
41
+ *
42
+ * Pass an array to run BOTH conventions (e.g. `["test", "tests"]`) in a
43
+ * package whose files are mid-migration between the two. This is an interim
44
+ * escape hatch until the repo-wide naming unification lands — see the README.
45
+ */
46
+ glob: TestGlob | TestGlob[];
47
+ /**
48
+ * Test environment. `"jsdom"` (default) for DOM component tests, `"node"`
49
+ * for framework-agnostic / SSR-only packages.
50
+ *
51
+ * @note jsdom is the interim environment. The forward path is Vitest browser
52
+ * mode with Playwright (see the svelte-ds-global migration); this factory
53
+ * will gain a `browser` option to drive that per-package flip without a
54
+ * config rewrite.
55
+ */
56
+ environment?: "jsdom" | "node";
57
+ /**
58
+ * When true, adds a second `node`-environment project that runs
59
+ * `**\/*.ssr.<glob>.tsx` files (client project excludes them). Mirrors the
60
+ * client/ssr split used by ds-global, hooks, head, router and tokens.
61
+ */
62
+ ssr?: boolean;
63
+ /**
64
+ * v8 coverage. `false` (default) attaches no coverage block — preserving the
65
+ * behaviour of packages that do not currently gate coverage. `true` enables
66
+ * coverage with 100/100/100/100 thresholds. Pass an object to override
67
+ * thresholds/include/exclude while keeping the 100% defaults for any omitted
68
+ * threshold.
69
+ */
70
+ coverage?: CoverageOption;
71
+ /** Setup files (e.g. `["./vitest.setup.ts"]`); omitted when empty. */
72
+ setupFiles?: string[];
73
+ /**
74
+ * Vite plugins to attach to each project. Vitest resolves plugins per
75
+ * project, so packages with a split pass their `[react()]` here.
76
+ */
77
+ // biome-ignore lint/suspicious/noExplicitAny: Vite 8 plugin types are incompatible with vitest's Vite 7 re-exports (matches per-package configs).
78
+ plugins?: any[] | PluginOption[];
79
+ }
80
+
81
+ /** Standard coverage excludes shared by every coverage-gated package. */
82
+ const BASE_COVERAGE_EXCLUDE = [
83
+ "**/index.ts",
84
+ "**/*.d.ts",
85
+ "**/types.ts",
86
+ ] as const;
87
+
88
+ const FULL_THRESHOLDS = {
89
+ branches: 100,
90
+ functions: 100,
91
+ lines: 100,
92
+ statements: 100,
93
+ } as const;
94
+
95
+ /**
96
+ * Build the v8 coverage block. Test files are always excluded (both `.test.`
97
+ * and `.tests.` plus their `.ssr.` variants) so coverage measures runtime code
98
+ * only. Thresholds default to 100% and are overridable per the storybook-config
99
+ * convention of "sensible defaults you pass through".
100
+ */
101
+ const buildCoverage = (
102
+ coverage: Exclude<CoverageOption, false>,
103
+ globs: readonly TestGlob[],
104
+ ): CoverageConfig => {
105
+ const overrides = coverage === true ? {} : coverage;
106
+ const testExclude = globs.flatMap((glob) => [
107
+ `**/*.${glob}.ts`,
108
+ `**/*.${glob}.tsx`,
109
+ `**/*.ssr.${glob}.ts`,
110
+ `**/*.ssr.${glob}.tsx`,
111
+ ]);
112
+ return {
113
+ provider: "v8",
114
+ include: overrides.include ?? ["src/**/*.{ts,tsx}"],
115
+ exclude: [
116
+ ...BASE_COVERAGE_EXCLUDE,
117
+ ...testExclude,
118
+ ...(overrides.exclude ?? []),
119
+ ],
120
+ thresholds: { ...FULL_THRESHOLDS, ...overrides.thresholds },
121
+ };
122
+ };
123
+
124
+ /**
125
+ * Canonical's standard Vitest `test` configuration for React packages.
126
+ *
127
+ * Returns the `test` block to spread into `defineConfig` from `vitest/config`.
128
+ * Every distinct React vitest posture in the monorepo is a point in this
129
+ * option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
130
+ * there is a single factory rather than per-shape variants.
131
+ *
132
+ * `import { reactTestConfig } from "@canonical/vitest-config-react";`
133
+ *
134
+ * @example jsdom component package (ds-app family)
135
+ * ```ts
136
+ * test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
137
+ * ```
138
+ * @example client + ssr split with 100% coverage (hooks, router)
139
+ * ```ts
140
+ * test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
141
+ * ```
142
+ * @example node-only adapter with overridden coverage include
143
+ * ```ts
144
+ * test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
145
+ * ```
146
+ */
147
+ export const reactTestConfig = ({
148
+ glob,
149
+ environment = "jsdom",
150
+ ssr = false,
151
+ coverage = false,
152
+ setupFiles,
153
+ plugins,
154
+ }: ReactTestConfigOptions): TestConfig => {
155
+ const hasSetup = setupFiles !== undefined && setupFiles.length > 0;
156
+ // Normalise to an array: a single convention is the common case, the array
157
+ // form covers packages whose files are split between `.test.` and `.tests.`.
158
+ const globs = Array.isArray(glob) ? glob : [glob];
159
+
160
+ // The client project: DOM (or node) tests, excluding the `.ssr.` variants
161
+ // when an ssr project will pick them up.
162
+ const clientTest: TestConfig = {
163
+ name: "client",
164
+ environment,
165
+ globals: true,
166
+ ...(hasSetup ? { setupFiles } : {}),
167
+ include: globs.flatMap((g) => [`src/**/*.${g}.ts`, `src/**/*.${g}.tsx`]),
168
+ ...(ssr ? { exclude: globs.map((g) => `src/**/*.ssr.${g}.tsx`) } : {}),
169
+ };
170
+
171
+ const coverageBlock =
172
+ coverage === false ? {} : { coverage: buildCoverage(coverage, globs) };
173
+
174
+ // No split: return a flat single-project test block (the common case).
175
+ if (!ssr) {
176
+ // The client project's identity (`name`) is only meaningful in a split, so
177
+ // a flat config omits it to match the hand-written single-project shape.
178
+ const { name: _name, ...flat } = clientTest;
179
+ return { ...flat, ...coverageBlock };
180
+ }
181
+
182
+ // Split: a jsdom/node client project plus a node ssr project that runs only
183
+ // the `.ssr.` files.
184
+ const ssrTest: TestConfig = {
185
+ name: "ssr",
186
+ environment: "node",
187
+ include: globs.map((g) => `src/**/*.ssr.${g}.tsx`),
188
+ };
189
+
190
+ return {
191
+ ...coverageBlock,
192
+ projects: [
193
+ { ...(plugins ? { plugins } : {}), test: clientTest },
194
+ { ...(plugins ? { plugins } : {}), test: ssrTest },
195
+ ],
196
+ };
197
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist/esm",
6
+ "declaration": true,
7
+ "declarationDir": "dist/types",
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node"]
12
+ }
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@canonical/typescript-config",
3
+ "compilerOptions": {
4
+ "baseUrl": "src",
5
+ "moduleResolution": "NodeNext",
6
+ "skipLibCheck": true,
7
+ "types": ["node"]
8
+ },
9
+ "include": ["src/**/*.ts"]
10
+ }