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.
- package/README.md +4 -2
- package/dist/assets/types/hmr.d.ts +2 -0
- package/dist/cli-entry.js +1 -1
- package/dist/data-table/cli.d.ts +2 -0
- package/dist/data-table/cli.d.ts.map +1 -0
- package/dist/data-table/cli.js +2 -0
- package/dist/node-hmr/runtime.d.ts +2 -0
- package/dist/node-hmr/runtime.d.ts.map +1 -0
- package/dist/node-hmr/runtime.js +2 -0
- package/dist/node-hmr/types.d.ts +2 -0
- package/dist/node-hmr.d.ts +2 -0
- package/dist/node-hmr.d.ts.map +1 -0
- package/dist/node-hmr.js +2 -0
- package/dist/ui/dev/refresh.d.ts +2 -0
- package/dist/ui/dev/refresh.d.ts.map +1 -0
- package/dist/ui/dev/refresh.js +2 -0
- package/dist/ui-hmr/assets.d.ts +2 -0
- package/dist/ui-hmr/assets.d.ts.map +1 -0
- package/dist/ui-hmr/assets.js +2 -0
- package/dist/ui-hmr/node.d.ts +3 -0
- package/dist/ui-hmr/node.d.ts.map +1 -0
- package/dist/ui-hmr/node.js +3 -0
- package/dist/ui-hmr/runtime/browser.d.ts +2 -0
- package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/browser.js +2 -0
- package/dist/ui-hmr/runtime/server.d.ts +2 -0
- package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/server.js +2 -0
- package/dist/ui-hmr.d.ts +2 -0
- package/dist/ui-hmr.d.ts.map +1 -0
- package/dist/ui-hmr.js +2 -0
- package/package.json +78 -126
- package/src/assets/README.md +322 -56
- package/src/assets/types/hmr.d.ts +2 -0
- package/src/cli/README.md +105 -1
- package/src/cookie/README.md +4 -4
- package/src/data-table/README.md +202 -68
- package/src/data-table/cli.ts +2 -0
- package/src/data-table-mysql/README.md +22 -18
- package/src/data-table-postgres/README.md +14 -14
- package/src/data-table-sqlite/README.md +38 -20
- package/src/fetch-proxy/README.md +25 -0
- package/src/node-fetch-server/README.md +11 -11
- package/src/node-hmr/README.md +307 -0
- package/src/node-hmr/runtime.ts +2 -0
- package/src/node-hmr/types.d.ts +2 -0
- package/src/node-hmr.ts +2 -0
- package/src/route-pattern/README.md +91 -8
- package/src/session-middleware/README.md +9 -7
- package/src/test/README.md +161 -115
- package/src/ui/README.md +74 -1
- package/src/ui/dev/refresh.ts +2 -0
- package/src/ui/test/README.md +151 -60
- package/src/ui-hmr/README.md +119 -0
- package/src/ui-hmr/assets.ts +2 -0
- package/src/ui-hmr/node.ts +3 -0
- package/src/ui-hmr/runtime/browser.ts +2 -0
- package/src/ui-hmr/runtime/server.ts +2 -0
- 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** -
|
|
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/:
|
|
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
|
-
|
|
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.
|
|
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
|
|
263
|
+
matcher.add('files/readme', null)
|
|
220
264
|
|
|
221
|
-
let matches = matcher.matchAll('https://example.com/files/readme
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
65
|
+
return createHtmlResponse(html`
|
|
64
66
|
<html>
|
|
65
67
|
<body>
|
|
66
68
|
<h1>Login</h1>
|
|
67
|
-
${typeof error === 'string' ?
|
|
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
|
|
88
|
+
return redirect('/login')
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
session.regenerateId()
|
|
90
92
|
session.set('userId', user.id)
|
|
91
93
|
|
|
92
|
-
return
|
|
94
|
+
return redirect('/dashboard')
|
|
93
95
|
})
|
|
94
96
|
|
|
95
97
|
router.post('/logout', ({ session }) => {
|
|
96
98
|
session.destroy()
|
|
97
|
-
return
|
|
99
|
+
return redirect('/')
|
|
98
100
|
})
|
|
99
101
|
```
|
|
100
102
|
|
package/src/test/README.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
113
|
+
```sh
|
|
114
|
+
remix test --config ./config/remix.ci.json
|
|
115
|
+
```
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
type: ['server', 'browser', 'e2e'],
|
|
159
|
+
### Focusing Tests
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
watch: false,
|
|
130
|
-
} satisfies RemixTestConfig
|
|
131
|
-
```
|
|
161
|
+
Use `.only` to focus a suite or test while developing:
|
|
132
162
|
|
|
133
|
-
|
|
163
|
+
```ts
|
|
164
|
+
describe.only('Cart routes', () => {
|
|
165
|
+
it('loads cart items', () => {})
|
|
166
|
+
})
|
|
134
167
|
|
|
135
|
-
|
|
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 --
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
242
|
+
concurrency: 1,
|
|
225
243
|
cwd: process.cwd(),
|
|
244
|
+
glob: { test: 'src/**/*.test.ts' },
|
|
245
|
+
type: ['server'],
|
|
226
246
|
})
|
|
227
247
|
```
|
|
228
248
|
|
|
229
|
-
|
|
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
|
|
454
|
+
Configure Playwright's browsers, timeouts, viewport, and other executable settings in
|
|
455
|
+
`playwright.config.ts`:
|
|
430
456
|
|
|
431
457
|
```ts
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 `
|
|
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.
|