remix 3.0.0-beta.5 → 3.0.0-beta.6

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.
Files changed (59) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/data-table/cli.js +2 -0
  7. package/dist/node-hmr/runtime.d.ts +2 -0
  8. package/dist/node-hmr/runtime.d.ts.map +1 -0
  9. package/dist/node-hmr/runtime.js +2 -0
  10. package/dist/node-hmr/types.d.ts +2 -0
  11. package/dist/node-hmr.d.ts +2 -0
  12. package/dist/node-hmr.d.ts.map +1 -0
  13. package/dist/node-hmr.js +2 -0
  14. package/dist/ui/dev/refresh.d.ts +2 -0
  15. package/dist/ui/dev/refresh.d.ts.map +1 -0
  16. package/dist/ui/dev/refresh.js +2 -0
  17. package/dist/ui-hmr/assets.d.ts +2 -0
  18. package/dist/ui-hmr/assets.d.ts.map +1 -0
  19. package/dist/ui-hmr/assets.js +2 -0
  20. package/dist/ui-hmr/node.d.ts +3 -0
  21. package/dist/ui-hmr/node.d.ts.map +1 -0
  22. package/dist/ui-hmr/node.js +3 -0
  23. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  24. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  25. package/dist/ui-hmr/runtime/browser.js +2 -0
  26. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  27. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  28. package/dist/ui-hmr/runtime/server.js +2 -0
  29. package/dist/ui-hmr.d.ts +2 -0
  30. package/dist/ui-hmr.d.ts.map +1 -0
  31. package/dist/ui-hmr.js +2 -0
  32. package/package.json +78 -126
  33. package/src/assets/README.md +322 -56
  34. package/src/assets/types/hmr.d.ts +2 -0
  35. package/src/cli/README.md +105 -1
  36. package/src/cookie/README.md +4 -4
  37. package/src/data-table/README.md +202 -68
  38. package/src/data-table/cli.ts +2 -0
  39. package/src/data-table-mysql/README.md +22 -18
  40. package/src/data-table-postgres/README.md +14 -14
  41. package/src/data-table-sqlite/README.md +38 -20
  42. package/src/fetch-proxy/README.md +25 -0
  43. package/src/node-fetch-server/README.md +11 -11
  44. package/src/node-hmr/README.md +307 -0
  45. package/src/node-hmr/runtime.ts +2 -0
  46. package/src/node-hmr/types.d.ts +2 -0
  47. package/src/node-hmr.ts +2 -0
  48. package/src/route-pattern/README.md +91 -8
  49. package/src/session-middleware/README.md +9 -7
  50. package/src/test/README.md +161 -115
  51. package/src/ui/README.md +74 -1
  52. package/src/ui/dev/refresh.ts +2 -0
  53. package/src/ui/test/README.md +151 -60
  54. package/src/ui-hmr/README.md +119 -0
  55. package/src/ui-hmr/assets.ts +2 -0
  56. package/src/ui-hmr/node.ts +3 -0
  57. package/src/ui-hmr/runtime/browser.ts +2 -0
  58. package/src/ui-hmr/runtime/server.ts +2 -0
  59. package/src/ui-hmr.ts +2 -0
@@ -8,7 +8,7 @@ Type-safe URL matching and href generation for JavaScript. `route-pattern` suppo
8
8
  - **Expressive** - Variables, wildcards, optionals, and search constraints
9
9
  - **Full URL support** - Match protocol, hostname, port, pathname, and search
10
10
  - **Simple & deterministic ranking** - Predictable left-to-right priority for static, variable, and wildcard patterns
11
- - **Fast** - Trie-based matching for scalable performance
11
+ - **Fast** - Indexed, bounded-state matching without variant expansion or regex backtracking
12
12
  - **Modular** - Import only the features you need to for smaller bundles
13
13
  - **Runtime agnostic** - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers
14
14
 
@@ -84,9 +84,14 @@ Protocol must be `http`, `https`, or `http(s)`:
84
84
 
85
85
  ```ts
86
86
  'users/:id' // matches /users/123
87
- 'blog/:year-:month-:day/:slug' // matches /blog/2024-01-15/hello
87
+ 'blog/:date/:slug' // matches /blog/2024-01-15/hello
88
+ 'files/:name.:ext' // matches /files/readme.md
88
89
  ```
89
90
 
91
+ Pathname variables possessively capture the largest non-empty run up to `/` or `.`. Hyphens are data, so UUIDs and slugs remain intact. A variable may have static text before it, but every path through following optionals must reach `/`, `.`, a wildcard, or the end of the hostname or pathname. Capture an inseparable value such as a date with one variable instead of `:year-:month-:day`.
92
+
93
+ Raw `/` and `.` are structural delimiters. Their percent-encoded forms remain data and are decoded in the resulting param. Static pattern text may use either decoded text or percent encoding, so `/café` and `/caf%C3%A9` match the same pathname text.
94
+
90
95
  **Wildcards** match multi-segment paths using `*name`:
91
96
 
92
97
  ```ts
@@ -95,6 +100,8 @@ Protocol must be `http`, `https`, or `http(s)`:
95
100
  'files/*' // matches any path under /files, but doesn't capture the wildcard value
96
101
  ```
97
102
 
103
+ Patterns may contain any number of wildcards when static text or a delimiter separates them. Adjacent wildcards such as `*left*right` are rejected because their capture boundary is ambiguous.
104
+
98
105
  **Optionals** make parts optional using `()`:
99
106
 
100
107
  ```ts
@@ -104,7 +111,9 @@ Protocol must be `http`, `https`, or `http(s)`:
104
111
  'api(/v:major(.:minor))' // matches /api, /api/v2, /api/v2.1
105
112
  ```
106
113
 
107
- While variables, wilcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
114
+ Optionals compile as state branches rather than concrete variants, so independent and nested optionals do not cause exponential matcher construction. Empty optionals and adjacent optional branches that give the same URL different capture schemas are rejected.
115
+
116
+ While variables, wildcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
108
117
 
109
118
  ```ts
110
119
  ':tenant.example.com/dashboard' // matches acme.example.com/dashboard
@@ -113,6 +122,19 @@ While variables, wilcards, and optionals are most prevalent in pathnames, you ca
113
122
  '(:locale.)example.com/docs(/:section)' // matches en.example.com/docs, en.example.com/docs/guides
114
123
  ```
115
124
 
125
+ Capture names may repeat. `params` uses the last participating capture in pattern order, while `paramsMeta` retains every participating capture:
126
+
127
+ ```ts
128
+ let matcher = createMatcher('/:id/:id')
129
+ let match = matcher.match('https://example.com/first/second')
130
+
131
+ match?.params
132
+ // { id: 'second' }
133
+
134
+ match?.paramsMeta.pathname.map(({ name, value }) => ({ name, value }))
135
+ // [{ name: 'id', value: 'first' }, { name: 'id', value: 'second' }]
136
+ ```
137
+
116
138
  **Escape characters** with `\`:
117
139
 
118
140
  ```ts
@@ -151,7 +173,21 @@ docsMatcher.match(url)?.params
151
173
  // Type safe params ^? { tenant: string | undefined, path: string, ext: string } | undefined
152
174
  ```
153
175
 
154
- Matchers accept absolute URL strings or `URL` objects. Relative strings such as `/blog/v3` are not accepted because matching uses the platform `URL` parser; wrap relative paths with a known origin before matching them.
176
+ Matchers accept absolute URL strings or `URL` objects. To match a relative URL reference, pass an absolute `baseURL`; the input is resolved with the same semantics as `new URL(input, baseURL)`, and the resolved URL is returned on the match.
177
+
178
+ ```ts
179
+ let match = blogMatcher.match('../blog/v3', {
180
+ baseURL: 'https://example.com/admin/settings',
181
+ })
182
+
183
+ match?.params
184
+ // { slug: 'v3' }
185
+
186
+ match?.url.href
187
+ // 'https://example.com/blog/v3'
188
+ ```
189
+
190
+ This works for root-relative, path-relative, query-relative, and network-path references. Without `baseURL`, string inputs must still be absolute.
155
191
 
156
192
  ### Match against multiple patterns
157
193
 
@@ -195,6 +231,14 @@ matcher.match('https://example.com/docs/Intro')?.params
195
231
  // { slug: 'Intro' }
196
232
  ```
197
233
 
234
+ Matchers limit individual pattern size, total matcher size, and the work performed by one match. Pattern and matcher sizes are measured in UTF-8 bytes. Direct package consumers may lower or raise individual limits. Exceeding one throws `MatcherResourceError` with structured `details` instead of silently abandoning matching:
235
+
236
+ ```ts
237
+ let matcher = createMultiMatcher({
238
+ limits: { maxPatternSize: 4096, maxMatchWork: 100_000 },
239
+ })
240
+ ```
241
+
198
242
  ### Ranking matches by specificity
199
243
 
200
244
  When multiple patterns match the same URL, `route-pattern` chooses the most specific match deterministically. Matches are ranked left-to-right, character-by-character:
@@ -216,12 +260,12 @@ import { descending } from 'remix/route-pattern/specificity'
216
260
  let matcher = createMultiMatcher()
217
261
  matcher.add('files/*path', null)
218
262
  matcher.add('files/:name', null)
219
- matcher.add('files/readme.md', null)
263
+ matcher.add('files/readme', null)
220
264
 
221
- let matches = matcher.matchAll('https://example.com/files/readme.md')
265
+ let matches = matcher.matchAll('https://example.com/files/readme')
222
266
 
223
267
  matches.sort(descending).map((match) => match.pattern.toString())
224
- // ['/files/readme.md', '/files/:name', '/files/*path']
268
+ // ['/files/readme', '/files/:name', '/files/*path']
225
269
  ```
226
270
 
227
271
  ## Generate hrefs
@@ -247,8 +291,41 @@ createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
247
291
  })
248
292
  // 'https://us-west.cdn.com/assets/images/logo.png'
249
293
 
250
- createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
294
+ createHref(
295
+ 'blog/:slug?ref=docs',
296
+ { slug: 'v3' },
297
+ {
298
+ searchParams: { utm_source: 'newsletter' },
299
+ },
300
+ )
251
301
  // '/blog/v3?utm_source=newsletter&ref=docs'
302
+
303
+ createHref('users/:id', { id: 'a.b' })
304
+ // '/users/a%2Eb' (the encoded dot remains variable data when matched)
305
+ ```
306
+
307
+ Pass `baseURL` to generate a path-relative reference to a same-origin route. Patterns with a different origin remain absolute.
308
+
309
+ ```ts
310
+ let baseURL = new URL('https://example.com/admin/settings')
311
+
312
+ createHref('users/:id', { id: '123' }, { baseURL })
313
+ // '../users/123'
314
+
315
+ createHref('https://cdn.example.com/assets/*path', { path: 'logo.svg' }, { baseURL })
316
+ // 'https://cdn.example.com/assets/logo.svg'
317
+ ```
318
+
319
+ The `searchParams` option accepts a plain object or `URLSearchParams`. Use `URLSearchParams` when duplicate keys or their order matter:
320
+
321
+ ```ts
322
+ let searchParams = new URLSearchParams([
323
+ ['tag', 'featured'],
324
+ ['tag', 'popular'],
325
+ ])
326
+
327
+ createHref('search', undefined, { searchParams })
328
+ // '/search?tag=featured&tag=popular'
252
329
  ```
253
330
 
254
331
  `createHref()` throws `CreateHrefError` when it cannot safely generate an href. The error exposes stable structured details on `error.details`; the string message is for humans.
@@ -300,7 +377,13 @@ The public support types are:
300
377
  - `RoutePatternCapture` from `remix/route-pattern`
301
378
  - `RoutePatternJSON` from `remix/route-pattern`
302
379
  - `CreateHrefErrorDetails` from `remix/route-pattern/href`
380
+ - `CreateHrefOptions` and `CreateHrefSearchParams` from `remix/route-pattern/href`
303
381
  - `MatchParamMeta` from `remix/route-pattern/match`
382
+ - `MatchOptions` from `remix/route-pattern/match`
383
+ - `MatcherLimits` from `remix/route-pattern/match`
384
+ - `MatcherResourceError` and `MatcherResourceErrorDetails` from `remix/route-pattern/match`
385
+
386
+ Literal patterns are validated and infer named params until the type-level parser reaches its 64-step complexity budget. Larger runtime-valid patterns remain accepted and fall back to safe general pattern types instead of risking a TypeScript excessive-instantiation error.
304
387
 
305
388
  ## Combine patterns
306
389
 
@@ -24,7 +24,6 @@ import { session } from 'remix/middleware/session'
24
24
 
25
25
  let sessionCookie = createCookie('__session', {
26
26
  secrets: ['s3cr3t'], // session cookies must be signed!
27
- httpOnly: true,
28
27
  secure: true,
29
28
  sameSite: 'lax',
30
29
  })
@@ -50,21 +49,24 @@ The middleware:
50
49
  Use `context.session` (or `context.get(Session)`) for normal session reads and writes.
51
50
 
52
51
  Note: The session cookie must be signed for security. This prevents tampering with the session data on the client.
52
+ Session cookies are HTTP-only by default.
53
53
 
54
54
  ### Login/Logout Flow
55
55
 
56
56
  A basic login/logout flow could look like this:
57
57
 
58
58
  ```ts
59
- import * as res from 'remix/router/response-helpers'
59
+ import { html } from 'remix/html-template'
60
+ import { createHtmlResponse } from 'remix/response/html'
61
+ import { redirect } from 'remix/response/redirect'
60
62
 
61
63
  router.get('/login', ({ session }) => {
62
64
  let error = session.get('error')
63
- return res.html(`
65
+ return createHtmlResponse(html`
64
66
  <html>
65
67
  <body>
66
68
  <h1>Login</h1>
67
- ${typeof error === 'string' ? <div class="error">${error}</div> : null}
69
+ ${typeof error === 'string' ? html`<div class="error">${error}</div>` : null}
68
70
  <form method="POST" action="/login">
69
71
  <input type="text" name="username" placeholder="Username" />
70
72
  <input type="password" name="password" placeholder="Password" />
@@ -83,18 +85,18 @@ router.post('/login', ({ get, session }) => {
83
85
  let user = authenticateUser(username, password)
84
86
  if (!user) {
85
87
  session.flash('error', 'Invalid username or password')
86
- return res.redirect('/login')
88
+ return redirect('/login')
87
89
  }
88
90
 
89
91
  session.regenerateId()
90
92
  session.set('userId', user.id)
91
93
 
92
- return res.redirect('/dashboard')
94
+ return redirect('/dashboard')
93
95
  })
94
96
 
95
97
  router.post('/logout', ({ session }) => {
96
98
  session.destroy()
97
- return res.redirect('/')
99
+ return redirect('/')
98
100
  })
99
101
  ```
100
102
 
@@ -12,7 +12,7 @@ A test framework for JavaScript and TypeScript projects.
12
12
  - Per-test and hook timeouts with `t.signal` abort support
13
13
  - Unified code coverage reporting across unit and E2E tests
14
14
  - Watch mode
15
- - Config file support (`remix-test.config.ts`)
15
+ - Static CLI configuration through `remix.json`
16
16
 
17
17
  ## Installation
18
18
 
@@ -42,127 +42,145 @@ Run tests with the CLI:
42
42
  remix test
43
43
  ```
44
44
 
45
- By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to override:
45
+ By default, `remix test` discovers all files matching `**/*.test{,.browser,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to override:
46
46
 
47
47
  ```sh
48
48
  remix test "src/**/*.test.ts"
49
49
  remix test "src/**/*.test.ts" "tests/**/*.test.tsx"
50
50
  ```
51
51
 
52
- Or, you may control via the `glob.test` config field/CLI arg. Each `glob.*` field accepts a single string or an array of patterns, and `--glob.*` flags can be repeated on the CLI.
52
+ You may also repeat the `--glob.*` flags. Positional globs take precedence over `--glob.test`.
53
53
 
54
54
  ### Config File
55
55
 
56
- Create a `remix-test.config.ts` (or `.js`) file at the root of your project (shown with default values):
56
+ Create an optional `remix.json` at the root of your project. It is parsed as JSONC, so comments and
57
+ trailing commas are allowed:
58
+
59
+ ```jsonc
60
+ {
61
+ "$schema": "https://remix.run/schemas/remix.json",
62
+ "test": {
63
+ "files": ["**/*.test{,.browser,.e2e}.{ts,tsx}"],
64
+ "browserFiles": ["**/*.test.browser.{ts,tsx}"],
65
+ "e2eFiles": ["**/*.test.e2e.{ts,tsx}"],
66
+ "exclude": ["node_modules/**", "dist/**"],
67
+ "type": ["server", "browser", "e2e"],
68
+ "only": ["/checkout/i"],
69
+
70
+ "concurrency": 2,
71
+ "pool": "forks",
72
+ "setup": "./test/setup.ts",
73
+ "watch": false,
74
+
75
+ "playwright": {
76
+ "echo": false,
77
+ "open": false,
78
+ "configFile": "./playwright.config.ts",
79
+ "projects": ["chromium", "firefox"],
80
+ },
57
81
 
58
- ```ts
59
- import type { RemixTestConfig } from 'remix/test'
60
-
61
- export default {
62
- // Browser options for E2E tests
63
- browser: {
64
- // Echo browser console output to the terminal
65
- echo: false,
66
- // Open browser (via playwright `headless:false`) and keep it open after tests
67
- // complete (useful for debugging)
68
- open: false,
82
+ "reporter": "spec",
83
+ "quiet": false,
84
+
85
+ "coverage": {
86
+ "enabled": true,
87
+ "dir": ".coverage",
88
+ "include": ["src/**"],
89
+ "exclude": ["src/**/*.test.ts"],
90
+ "statements": 80,
91
+ "lines": 80,
92
+ "branches": 80,
93
+ "functions": 80,
94
+ },
69
95
  },
96
+ }
97
+ ```
70
98
 
71
- // Max number of concurrent test workers (default `os.availableParallelism()`)
72
- concurrency: 2,
73
-
74
- // Pool for server and E2E test files ("forks", "threads")
75
- pool: 'forks',
76
-
77
- // Code coverage options
78
- coverage: {
79
- // Enable coverage reporting
80
- enabled: true,
81
- // Output directory (default: ".coverage")
82
- dir: '.coverage',
83
- // Glob pattern(s) to include/exclude
84
- include: 'src/**',
85
- exclude: 'src/**/*.test.ts',
86
- // Minimum thresholds (%)
87
- statements: 80,
88
- lines: 80,
89
- branches: 80,
90
- functions: 80,
91
- },
99
+ Every field is optional. Relative paths and globs resolve from the directory containing the config
100
+ file. Explicit CLI flags and positional globs take precedence. Repeatable flags replace configured
101
+ arrays, and nested Playwright and coverage values merge by field.
92
102
 
93
- // Glob pattern(s) identifying test files
94
- glob: {
95
- // All test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}").
96
- test: '**/*.test{,.browser,.e2e}.ts',
97
- // Browser test files (default: "**/*.test.browser.{ts,tsx}")
98
- browser: '**/*.test.browser.ts',
99
- // E2E test files (default: "**/*.test.e2e.{ts,tsx}")
100
- e2e: '**/*.test.e2e.ts',
101
- },
103
+ `remix-test.config.ts` and `remix-test.config.js` are no longer discovered. Move their static values
104
+ under `remix.json#test`. Move inline Playwright configuration into `playwright.config.ts` and point
105
+ `playwright.configFile` at it. Use slash-delimited strings for regular expressions, and use CLI flags
106
+ or package scripts for environment-specific overrides.
102
107
 
103
- // Playwright configuration for E2E tests, or string path to an existing
104
- // config file on disk
105
- playwrightConfig: {
106
- projects: [
107
- { name: 'chromium', use: { browserName: 'chromium' } },
108
- { name: 'firefox', use: { browserName: 'firefox' } },
109
- ],
110
- use: {
111
- navigationTimeout: 5_000,
112
- actionTimeout: 5_000,
113
- },
114
- },
108
+ ### CLI Options
115
109
 
116
- // Playwright project(s) to run E2E tests for
117
- project: 'chromium',
110
+ Use the global `--config` flag to select another Remix config file. The flag path resolves from the
111
+ current working directory:
118
112
 
119
- // Test reporter ("spec", "files", "tap", "dot")
120
- reporter: 'spec',
113
+ ```sh
114
+ remix test --config ./config/remix.ci.json
115
+ ```
121
116
 
122
- // Path to a setup module (see Setup section below)
123
- setup: './test/setup.ts',
117
+ You may specify any test setting as a CLI flag. Boolean settings have negative forms so configured
118
+ `true` values can be disabled explicitly:
119
+
120
+ | Flag | Short |
121
+ | --------------------------- | ----- |
122
+ | `--browser.echo` | |
123
+ | `--no-browser.echo` | |
124
+ | `--browser.open` | |
125
+ | `--no-browser.open` | |
126
+ | `--concurrency <n>` | `-c` |
127
+ | `--coverage` | |
128
+ | `--no-coverage` | |
129
+ | `--coverage.dir <path>` | |
130
+ | `--coverage.include` | |
131
+ | `--coverage.exclude` | |
132
+ | `--coverage.statements` | |
133
+ | `--coverage.lines` | |
134
+ | `--coverage.branches` | |
135
+ | `--coverage.functions` | |
136
+ | `--glob.test` | |
137
+ | `--glob.browser` | |
138
+ | `--glob.e2e` | |
139
+ | `--glob.exclude` | |
140
+ | `--playwrightConfig <path>` | |
141
+ | `--only <pattern>` | |
142
+ | `--pool <forks\|threads>` | |
143
+ | `--project <name>` | `-p` |
144
+ | `--quiet` | `-q` |
145
+ | `--no-quiet` | |
146
+ | `--reporter <name>` | `-r` |
147
+ | `--setup <path>` | `-s` |
148
+ | `--type <name>` | `-t` |
149
+ | `--watch` | `-w` |
150
+ | `--no-watch` | |
151
+
152
+ The standalone `remix-test` executable is no longer installed. Update scripts to use the main Remix CLI:
153
+
154
+ ```diff
155
+ - "test": "remix-test --type server"
156
+ + "test": "remix test --type server"
157
+ ```
124
158
 
125
- // Test type(s) to run ("server", "browser", "e2e")
126
- type: ['server', 'browser', 'e2e'],
159
+ ### Focusing Tests
127
160
 
128
- // Watch for file changes and re-run
129
- watch: false,
130
- } satisfies RemixTestConfig
131
- ```
161
+ Use `.only` to focus a suite or test while developing:
132
162
 
133
- ### CLI Options
163
+ ```ts
164
+ describe.only('Cart routes', () => {
165
+ it('loads cart items', () => {})
166
+ })
134
167
 
135
- You can point to a different config file location with the `--config` flag:
168
+ describe('Checkout routes', () => {
169
+ it.only('redirects anonymous users', () => {})
170
+ })
171
+ ```
172
+
173
+ Use `--only <pattern>` to focus tests from the CLI without editing source. Plain patterns are case-insensitive JavaScript regular expressions matched against suite names and full test names:
136
174
 
137
175
  ```sh
138
- remix test --config ./tests/config.ts
176
+ remix test --only 'Cart routes'
177
+ remix test --only 'Checkout routes > redirects anonymous users'
178
+ remix test --only '/anonymous users$/'
139
179
  ```
140
180
 
141
- You may also specify any config field as a CLI flag which will take precedence over config file values:
142
-
143
- | Flag | Short |
144
- | --------------------------- | --------- | --- |
145
- | `--browser.echo` | |
146
- | `--browser.open` | |
147
- | `--concurrency <n>` | `-c` |
148
- | `--coverage` | |
149
- | `--coverage.dir <path>` | |
150
- | `--coverage.include` | |
151
- | `--coverage.exclude` | |
152
- | `--coverage.statements` | |
153
- | `--coverage.lines` | |
154
- | `--coverage.branches` | |
155
- | `--coverage.functions` | |
156
- | `--glob.test` | |
157
- | `--glob.browser` | |
158
- | `--glob.e2e` | |
159
- | `--playwrightConfig <path>` | |
160
- | `--pool <forks | threads>` | |
161
- | `--project <name>` | `-p` |
162
- | `--reporter <name>` | `-r` |
163
- | `--setup <path>` | `-s` |
164
- | `--type <name>` | `-t` |
165
- | `--watch` | `-w` |
181
+ Slash-delimited CLI and `remix.json` patterns preserve their flags, so `/pattern/` is case-sensitive and `/pattern/i` is case-insensitive. Plain string patterns are case-insensitive. Programmatic callers may also pass `RegExp` values.
182
+
183
+ Full test names join nested `describe` names and the test name with `>`. For example, `describe('Cart routes', () => describe('loader', () => it('loads cart items', ...)))` has the full test name `Cart routes > loader > loads cart items`.
166
184
 
167
185
  ### Setup
168
186
 
@@ -221,12 +239,19 @@ suite('My Test Suite', () => {
221
239
  import { runRemixTest } from 'remix/test/cli'
222
240
 
223
241
  let exitCode = await runRemixTest({
224
- argv: ['--type', 'server'],
242
+ concurrency: 1,
225
243
  cwd: process.cwd(),
244
+ glob: { test: 'src/**/*.test.ts' },
245
+ type: ['server'],
226
246
  })
227
247
  ```
228
248
 
229
- `runRemixTest()` returns the runner exit code. The `remix test` bin wrapper calls `process.exit()` with that code when the run finishes so open workers, browsers, or project handles cannot keep the CLI alive.
249
+ The programmatic runner accepts structured options only; it does not discover or load configuration
250
+ files. Programmatic callers may pass richer values such as an inline Playwright config or `RegExp`
251
+ test-name filters. The main Remix CLI owns `remix.json` loading and passes the resolved options to the
252
+ runner.
253
+
254
+ `runRemixTest()` does not read `process.argv` or terminate the process; it returns the runner exit code. The main `remix` executable owns argument parsing and passes the final code to `process.exit()` so open workers, browsers, or project handles cannot keep the CLI alive. `@remix-run/test` does not install a standalone executable.
230
255
 
231
256
  ### Test Context
232
257
 
@@ -426,28 +451,49 @@ describe('checkout', () => {
426
451
  })
427
452
  ```
428
453
 
429
- Configure Playwright (browsers, timeouts, viewport, etc.) via `playwrightConfig` in your config file:
454
+ Configure Playwright's browsers, timeouts, viewport, and other executable settings in
455
+ `playwright.config.ts`:
430
456
 
431
457
  ```ts
432
- export default {
433
- playwrightConfig: {
434
- projects: [
435
- { name: 'chromium', use: { browserName: 'chromium' } },
436
- { name: 'firefox', use: { browserName: 'firefox' } },
437
- { name: 'webkit', use: { browserName: 'webkit' } },
438
- ],
439
- use: {
440
- navigationTimeout: 5_000,
441
- actionTimeout: 5_000,
442
- },
458
+ import { defineConfig } from 'playwright/test'
459
+
460
+ export default defineConfig({
461
+ projects: [
462
+ { name: 'chromium', use: { browserName: 'chromium' } },
463
+ { name: 'firefox', use: { browserName: 'firefox' } },
464
+ { name: 'webkit', use: { browserName: 'webkit' } },
465
+ ],
466
+ use: {
467
+ navigationTimeout: 5_000,
468
+ actionTimeout: 5_000,
443
469
  },
470
+ })
471
+ ```
444
472
 
445
- // Or, point to an existing playwright config file
446
- // playwrightConfig: './playwright.config.ts'
447
- } satisfies RemixTestConfig
473
+ Reference it from `remix.json` when it is not at the default location:
474
+
475
+ ```jsonc
476
+ {
477
+ "test": {
478
+ "playwright": {
479
+ "configFile": "./playwright.config.ts",
480
+ },
481
+ },
482
+ }
448
483
  ```
449
484
 
450
- Set `browser.open: true` to keep the browser open after tests finish — useful for debugging failures.
485
+ Set `test.playwright.open` to `true`, or pass `--browser.open`, to keep the browser open after tests
486
+ finish—useful for debugging failures.
487
+
488
+ ## Related Packages
489
+
490
+ - [`assert`](https://github.com/remix-run/remix/tree/main/packages/assert) provides assertions that work in server and browser tests.
491
+ - [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) provides the `remix/ui/test` browser rendering utilities.
492
+
493
+ ## Related Work
494
+
495
+ - [Playwright](https://playwright.dev) provides the browser automation used by browser and E2E tests.
496
+ - [Node.js test runner](https://nodejs.org/api/test.html) provides prior art for the test framework API and reporting model.
451
497
 
452
498
  ## License
453
499
 
package/src/ui/README.md CHANGED
@@ -4,7 +4,7 @@ Runtime UI primitives for Remix apps, including the component runtime, server re
4
4
 
5
5
  ## Features
6
6
 
7
- - Component runtime APIs for rendering, hydration, frame navigation, and JSX
7
+ - Component runtime APIs for rendering, hydration, link and form frame navigation, and JSX
8
8
  - Server rendering APIs for streaming Remix UI trees and frames
9
9
  - `mix` composition with event, ref, CSS, and animation helpers
10
10
  - Headless behavior primitives for controls such as menus, listboxes, popovers, selects, and comboboxes
@@ -79,6 +79,79 @@ function Actions() {
79
79
  }
80
80
  ```
81
81
 
82
+ ## Frame Navigation
83
+
84
+ Configure `resolveFrame` once to progressively enhance same-origin links and forms. The resolver owns the fetch request and receives native submission metadata for non-GET forms:
85
+
86
+ ```tsx
87
+ import type { ResolveFrameOptions } from 'remix/ui'
88
+ import { run } from 'remix/ui'
89
+
90
+ let app = run({
91
+ async loadModule(moduleUrl, exportName) {
92
+ let mod = await import(moduleUrl)
93
+ return mod[exportName]
94
+ },
95
+ async resolveFrame(src, options) {
96
+ return fetch(src, {
97
+ headers: { Accept: 'text/html', 'X-Remix-Frame': 'true' },
98
+ method: options?.method,
99
+ body: getRequestBody(options),
100
+ signal: options?.signal,
101
+ })
102
+ },
103
+ })
104
+
105
+ function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
106
+ let formData = options?.formData
107
+ if (!formData) return
108
+ if (options.encType !== 'application/x-www-form-urlencoded') return formData
109
+
110
+ let body = new URLSearchParams()
111
+ for (let [name, value] of formData) {
112
+ body.append(name, typeof value === 'string' ? value : value.name)
113
+ }
114
+ return body
115
+ }
116
+
117
+ await app.ready()
118
+ ```
119
+
120
+ Forms remain ordinary HTML forms before the runtime starts. Add `rmx-target` to reload a named frame, or `rmx-document` to require a full-document submission:
121
+
122
+ ```tsx
123
+ import { Frame } from 'remix/ui'
124
+
125
+ function AccountPage() {
126
+ return () => (
127
+ <>
128
+ <Frame name="account" src="/account/edit" />
129
+ <form action="/account/edit" method="post" rmx-target="account">
130
+ <label for="display-name">Display name</label>
131
+ <input id="display-name" name="displayName" required />
132
+ <button type="submit">Save</button>
133
+ </form>
134
+ </>
135
+ )
136
+ }
137
+ ```
138
+
139
+ Native constraint validation and submitter overrides still apply. GET form values arrive in `src`; non-GET forms provide `formData`, `method`, and `encType` to the resolver. See [Frames](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#form-navigation) for targeting, history behavior, request encoding, opt-outs, and server response guidance.
140
+
141
+ Use `rmx-history="push|replace"` on an enhanced anchor or form to control how the navigation updates history. This can override the automatic replacement used for non-GET form submissions to the current URL.
142
+
143
+ ## Preserving Client-Owned DOM
144
+
145
+ Use `rmx-preserve-dom` on the smallest element whose live DOM should belong to client code after initial render, such as a custom element or third-party widget:
146
+
147
+ ```tsx
148
+ <pagefind-ui data-key="search" rmx-preserve-dom>
149
+ <button type="button">Search</button>
150
+ </pagefind-ui>
151
+ ```
152
+
153
+ Remix UI still renders the element's children during SSR and still hydrates any initial client entries inside it. On later frame reloads, matched `rmx-preserve-dom` elements keep their current attributes and children instead of accepting incoming DOM updates. See [Preserving client-owned DOM](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#preserving-client-owned-dom) for guidance and caveats.
154
+
82
155
  ## Cascade Layers
83
156
 
84
157
  Remix UI emits generated `css(...)` rules under the `rmx` cascade layer. Unlayered CSS outranks layered CSS, so use explicit layer order when mixing Remix UI with global styles.
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/ui/dev/refresh'