@remix-run/test 0.1.0 → 0.3.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.
Files changed (159) hide show
  1. package/README.md +161 -50
  2. package/dist/app/client/entry.d.ts +2 -0
  3. package/dist/app/client/entry.d.ts.map +1 -0
  4. package/dist/app/client/entry.js +328 -0
  5. package/dist/app/client/iframe.d.ts +2 -0
  6. package/dist/app/client/iframe.d.ts.map +1 -0
  7. package/dist/app/client/iframe.js +22 -0
  8. package/dist/app/server.d.ts +6 -0
  9. package/dist/app/server.d.ts.map +1 -0
  10. package/dist/app/server.js +303 -0
  11. package/dist/cli-entry.d.ts +3 -0
  12. package/dist/cli-entry.d.ts.map +1 -0
  13. package/dist/cli-entry.js +14 -0
  14. package/dist/cli.d.ts +7 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +319 -140
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/lib/colors.d.ts +2 -0
  20. package/dist/lib/colors.d.ts.map +1 -0
  21. package/dist/lib/colors.js +2 -0
  22. package/dist/lib/config.d.ts +59 -14
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +181 -38
  25. package/dist/lib/context.d.ts +37 -13
  26. package/dist/lib/context.d.ts.map +1 -1
  27. package/dist/lib/context.js +19 -3
  28. package/dist/lib/coverage-loader.d.ts +16 -0
  29. package/dist/lib/coverage-loader.d.ts.map +1 -0
  30. package/dist/lib/coverage-loader.js +20 -0
  31. package/dist/lib/coverage.d.ts +28 -0
  32. package/dist/lib/coverage.d.ts.map +1 -0
  33. package/dist/lib/coverage.js +212 -0
  34. package/dist/lib/executor.d.ts +3 -26
  35. package/dist/lib/executor.d.ts.map +1 -1
  36. package/dist/lib/executor.js +11 -6
  37. package/dist/lib/fake-timers.d.ts +13 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +64 -0
  40. package/dist/lib/import-module.d.ts +2 -0
  41. package/dist/lib/import-module.d.ts.map +1 -0
  42. package/dist/lib/import-module.js +38 -0
  43. package/dist/lib/normalize.d.ts +2 -0
  44. package/dist/lib/normalize.d.ts.map +1 -0
  45. package/dist/lib/{utils.js → normalize.js} +0 -9
  46. package/dist/lib/playwright.d.ts +1 -1
  47. package/dist/lib/playwright.d.ts.map +1 -1
  48. package/dist/lib/playwright.js +5 -8
  49. package/dist/lib/reporters/dot.d.ts +1 -2
  50. package/dist/lib/reporters/dot.d.ts.map +1 -1
  51. package/dist/lib/reporters/dot.js +12 -1
  52. package/dist/lib/reporters/files.d.ts +1 -2
  53. package/dist/lib/reporters/files.d.ts.map +1 -1
  54. package/dist/lib/reporters/files.js +12 -1
  55. package/dist/lib/reporters/index.d.ts +4 -5
  56. package/dist/lib/reporters/index.d.ts.map +1 -1
  57. package/dist/lib/reporters/index.js +3 -3
  58. package/dist/lib/reporters/results.d.ts +30 -0
  59. package/dist/lib/reporters/results.d.ts.map +1 -0
  60. package/dist/lib/reporters/results.js +1 -0
  61. package/dist/lib/reporters/spec.d.ts +1 -2
  62. package/dist/lib/reporters/spec.d.ts.map +1 -1
  63. package/dist/lib/reporters/spec.js +12 -1
  64. package/dist/lib/reporters/tap.d.ts +1 -2
  65. package/dist/lib/reporters/tap.d.ts.map +1 -1
  66. package/dist/lib/reporters/tap.js +11 -1
  67. package/dist/lib/runner-browser.d.ts +21 -0
  68. package/dist/lib/runner-browser.d.ts.map +1 -0
  69. package/dist/lib/runner-browser.js +123 -0
  70. package/dist/lib/runner.d.ts +24 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +216 -38
  73. package/dist/lib/runtime.d.ts +2 -0
  74. package/dist/lib/runtime.d.ts.map +1 -0
  75. package/dist/lib/runtime.js +2 -0
  76. package/dist/lib/ts-transform.d.ts +4 -0
  77. package/dist/lib/ts-transform.d.ts.map +1 -0
  78. package/dist/lib/ts-transform.js +29 -0
  79. package/dist/lib/worker-e2e-file.d.ts +11 -0
  80. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  81. package/dist/lib/worker-e2e-file.js +69 -0
  82. package/dist/lib/worker-e2e.js +11 -46
  83. package/dist/lib/worker-process.d.ts +2 -0
  84. package/dist/lib/worker-process.d.ts.map +1 -0
  85. package/dist/lib/worker-process.js +55 -0
  86. package/dist/lib/worker-results.d.ts +3 -0
  87. package/dist/lib/worker-results.d.ts.map +1 -0
  88. package/dist/lib/worker-results.js +20 -0
  89. package/dist/lib/worker-server.d.ts +10 -0
  90. package/dist/lib/worker-server.d.ts.map +1 -0
  91. package/dist/lib/worker-server.js +113 -0
  92. package/dist/lib/worker.js +7 -28
  93. package/dist/test/coverage/fixture.d.ts +5 -0
  94. package/dist/test/coverage/fixture.d.ts.map +1 -0
  95. package/dist/test/coverage/fixture.js +32 -0
  96. package/dist/test/coverage/test-browser.d.ts +2 -0
  97. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  98. package/dist/test/coverage/test-browser.js +24 -0
  99. package/dist/test/coverage/test-e2e.d.ts +2 -0
  100. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  101. package/dist/test/coverage/test-e2e.js +60 -0
  102. package/dist/test/coverage/test-unit.d.ts +2 -0
  103. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  104. package/dist/test/coverage/test-unit.js +27 -0
  105. package/dist/test/framework.test.browser.d.ts +2 -0
  106. package/dist/test/framework.test.browser.d.ts.map +1 -0
  107. package/dist/test/framework.test.browser.js +107 -0
  108. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  109. package/dist/test/framework.test.e2e.js +34 -0
  110. package/package.json +30 -9
  111. package/src/app/client/entry.ts +357 -0
  112. package/src/app/client/iframe.ts +18 -0
  113. package/src/app/server.ts +336 -0
  114. package/src/cli-entry.ts +15 -0
  115. package/src/cli.ts +382 -145
  116. package/src/index.ts +2 -1
  117. package/src/lib/colors.ts +3 -0
  118. package/src/lib/config.ts +266 -54
  119. package/src/lib/context.ts +59 -17
  120. package/src/lib/coverage-loader.ts +31 -0
  121. package/src/lib/coverage.ts +320 -0
  122. package/src/lib/executor.ts +18 -35
  123. package/src/lib/fake-timers.ts +89 -0
  124. package/src/lib/import-module.ts +39 -0
  125. package/src/lib/{utils.ts → normalize.ts} +0 -18
  126. package/src/lib/playwright.ts +5 -7
  127. package/src/lib/reporters/dot.ts +12 -2
  128. package/src/lib/reporters/files.ts +12 -2
  129. package/src/lib/reporters/index.ts +4 -5
  130. package/src/lib/reporters/results.ts +29 -0
  131. package/src/lib/reporters/spec.ts +12 -2
  132. package/src/lib/reporters/tap.ts +11 -2
  133. package/src/lib/runner-browser.ts +171 -0
  134. package/src/lib/runner.ts +308 -53
  135. package/src/lib/runtime.ts +2 -0
  136. package/src/lib/ts-transform.ts +36 -0
  137. package/src/lib/worker-e2e-file.ts +98 -0
  138. package/src/lib/worker-e2e.ts +14 -49
  139. package/src/lib/worker-process.ts +69 -0
  140. package/src/lib/worker-results.ts +22 -0
  141. package/src/lib/worker-server.ts +123 -0
  142. package/src/lib/worker.ts +8 -28
  143. package/src/test/coverage/fixture.ts +34 -0
  144. package/src/test/coverage/test-browser.ts +29 -0
  145. package/src/test/coverage/test-e2e.ts +70 -0
  146. package/src/test/coverage/test-unit.ts +32 -0
  147. package/tsconfig.json +3 -1
  148. package/dist/lib/e2e-server.d.ts +0 -11
  149. package/dist/lib/e2e-server.d.ts.map +0 -1
  150. package/dist/lib/e2e-server.js +0 -15
  151. package/dist/lib/framework.test.d.ts +0 -2
  152. package/dist/lib/framework.test.d.ts.map +0 -1
  153. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  154. package/dist/lib/framework.test.e2e.js +0 -29
  155. package/dist/lib/framework.test.js +0 -283
  156. package/dist/lib/utils.d.ts +0 -16
  157. package/dist/lib/utils.d.ts.map +0 -1
  158. package/src/lib/e2e-server.ts +0 -28
  159. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
@@ -0,0 +1,336 @@
1
+ import { init as initEsModuleLexer, parse as parseEsModule } from 'es-module-lexer'
2
+ import MagicString from 'magic-string'
3
+ import * as fsp from 'node:fs/promises'
4
+ import * as http from 'node:http'
5
+ import { createRequire } from 'node:module'
6
+ import * as path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { SourceMapConsumer, SourceMapGenerator } from 'source-map-js'
9
+ import { getBrowserTestRootDir, IS_RUNNING_FROM_SRC } from '../lib/config.ts'
10
+ import { transformTypeScript } from '../lib/ts-transform.ts'
11
+
12
+ export async function startServer(
13
+ browserFiles: string[],
14
+ ): Promise<{ server: http.Server; port: number }> {
15
+ let handle = createRequestHandler(browserFiles)
16
+ let port = 44101
17
+
18
+ let lastError: unknown
19
+ for (let i = 0; i < 5; i++) {
20
+ try {
21
+ let server = http.createServer((req, res) => {
22
+ handle(req, res).catch((error) => {
23
+ console.error(`[remix-test] Unhandled error for ${req.url}:`, error)
24
+ if (!res.headersSent) {
25
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
26
+ }
27
+ if (!res.writableEnded) res.end()
28
+ })
29
+ })
30
+ await new Promise<void>((resolve, reject) => {
31
+ server.once('error', reject)
32
+ server.listen(port, () => {
33
+ server.removeListener('error', reject)
34
+ console.log(`Test server running on http://localhost:${port}`)
35
+ resolve()
36
+ })
37
+ })
38
+ return { server, port }
39
+ } catch (error: any) {
40
+ if (error.code !== 'EADDRINUSE') throw error
41
+ lastError = error
42
+ console.log(`Port ${port} is in use, trying another port...`)
43
+ port += 1
44
+ }
45
+ }
46
+
47
+ throw lastError
48
+ }
49
+
50
+ function createRequestHandler(
51
+ browserFiles: string[],
52
+ ): (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> {
53
+ let rootDir = getBrowserTestRootDir()
54
+ let srcDir = IS_RUNNING_FROM_SRC
55
+ ? // Up one directory from src/app/
56
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
57
+ : // Directory of the published index.js file
58
+ path.dirname(fileURLToPath(import.meta.resolve('@remix-run/test')))
59
+ let clientDir = path.join(srcDir, 'app', 'client')
60
+ let scriptExt = IS_RUNNING_FROM_SRC ? 'ts' : 'js'
61
+ let entryUrl = filePathToUrl(path.join(clientDir, `entry.${scriptExt}`), rootDir)
62
+ let iframeUrl = filePathToUrl(path.join(clientDir, `iframe.${scriptExt}`), rootDir)
63
+ if (!entryUrl || !iframeUrl) {
64
+ throw new Error(`Harness scripts in ${clientDir} are outside rootDir ${rootDir}`)
65
+ }
66
+
67
+ let testPaths = browserFiles.map((f) => filePathToUrl(f, rootDir)!)
68
+
69
+ return async (req, res) => {
70
+ let url = new URL(req.url ?? '/', 'http://localhost')
71
+
72
+ if (req.method !== 'GET') {
73
+ sendText(res, 405, 'Method Not Allowed')
74
+ return
75
+ }
76
+
77
+ if (url.pathname === '/') {
78
+ let setupJson = JSON.stringify({ testPaths, baseDir: process.cwd() })
79
+ let body =
80
+ `<script type="application/json" id="test-setup">` +
81
+ `${escapeJsonForScript(setupJson)}` +
82
+ `</script>` +
83
+ `<div id="test-root"></div>` +
84
+ `<script type="module" src="${entryUrl}"></script>`
85
+ sendHtml(res, 'Tests', body)
86
+ return
87
+ }
88
+
89
+ if (url.pathname === '/iframe') {
90
+ let test = decodeURIComponent(url.searchParams.get('file') || '')
91
+ sendHtml(res, `Test: ${test}`, `<script type="module" src="${iframeUrl}"></script>`)
92
+ return
93
+ }
94
+
95
+ if (url.pathname.startsWith('/scripts/')) {
96
+ let filePath = urlPathToFilePath(url.pathname, rootDir)
97
+ if (filePath) {
98
+ try {
99
+ await serveScript(res, filePath, url.pathname, rootDir)
100
+ return
101
+ } catch (error) {
102
+ console.error(`[remix-test] Error serving ${url.pathname}:`, error)
103
+ sendText(res, 500, String(error))
104
+ return
105
+ }
106
+ }
107
+ }
108
+
109
+ sendText(res, 404, 'Not found')
110
+ }
111
+ }
112
+
113
+ function filePathToUrl(filePath: string, rootDir: string): string | null {
114
+ let rel = path.relative(rootDir, filePath)
115
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return null
116
+ return '/scripts/' + rel.split(path.sep).join('/')
117
+ }
118
+
119
+ // `/scripts/<rel>` → `<rootDir>/<rel>` (URL space mirrors the filesystem
120
+ // rooted at rootDir, with `..` segments rejected so requests can't escape).
121
+ function urlPathToFilePath(urlPath: string, rootDir: string): string | null {
122
+ if (!urlPath.startsWith('/scripts/')) return null
123
+ let relative = urlPath.slice('/scripts/'.length)
124
+ if (!relative) return null
125
+ let filePath = path.resolve(rootDir, relative)
126
+ if (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep)) return null
127
+ return filePath
128
+ }
129
+
130
+ const TS_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts'])
131
+ const JS_EXTS = new Set(['.js', '.mjs', '.cjs', '.jsx'])
132
+
133
+ async function serveScript(
134
+ res: http.ServerResponse,
135
+ filePath: string,
136
+ urlPath: string,
137
+ rootDir: string,
138
+ ): Promise<void> {
139
+ let ext = path.extname(filePath)
140
+ let isTs = TS_EXTS.has(ext)
141
+ let isJs = JS_EXTS.has(ext)
142
+ if (!isTs && !isJs) {
143
+ sendText(res, 400, `Unsupported script extension "${ext}" for ${urlPath}`)
144
+ return
145
+ }
146
+
147
+ let source = await fsp.readFile(filePath, 'utf-8')
148
+ let code: string
149
+ if (isTs) {
150
+ try {
151
+ let result = await transformTypeScript(source, filePath)
152
+ code = result.code
153
+ } catch (error) {
154
+ let msg = error instanceof Error ? error.message : String(error)
155
+ console.error(`[remix-test] Failed to transform ${urlPath}: ${msg}`)
156
+ sendText(res, 500, msg)
157
+ return
158
+ }
159
+ } else {
160
+ code = source
161
+ }
162
+
163
+ try {
164
+ code = await rewriteImports(code, filePath, rootDir)
165
+ } catch (error) {
166
+ let msg = error instanceof Error ? error.message : String(error)
167
+ console.error(`[remix-test] Failed to rewrite imports for ${urlPath}: ${msg}`)
168
+ sendText(res, 500, msg)
169
+ return
170
+ }
171
+
172
+ res.writeHead(200, { 'Content-Type': 'application/javascript' })
173
+ res.end(code)
174
+ }
175
+
176
+ // Rewrite every import/export specifier in the (already-transformed) source so
177
+ // it points at an absolute `/scripts/<rel>` URL the harness can serve. Bare
178
+ // specifiers go through Node's resolver; relative specifiers resolve against
179
+ // the importer file's directory. This keeps the URL space === filesystem
180
+ // layout so harness scripts (under clientDir) and source files (anywhere
181
+ // under rootDir) can import each other without URL-relative confusion.
182
+ //
183
+ // Uses es-module-lexer (purpose-built ESM scanner) + magic-string so that the
184
+ // edits compose cleanly with the inline TS→JS source map from
185
+ // transformTypeScript: the resulting inline map is a true rewrittenJS → TS
186
+ // map, not just the original TS → JS map slapped on top of mutated bytes.
187
+ async function rewriteImports(
188
+ code: string,
189
+ importerFile: string,
190
+ rootDir: string,
191
+ ): Promise<string> {
192
+ await initEsModuleLexer
193
+
194
+ let { code: codeNoMap, map: tsToJsMap } = extractInlineSourceMap(code)
195
+ let [imports] = parseEsModule(codeNoMap)
196
+ let s = new MagicString(codeNoMap)
197
+ let edited = false
198
+
199
+ for (let imp of imports) {
200
+ // n is the parsed specifier value (with escapes resolved); undefined when
201
+ // the dynamic import argument isn't a static string literal.
202
+ if (imp.n == null) continue
203
+ let url = resolveSpecifier(imp.n, importerFile, rootDir)
204
+ if (url == null || url === imp.n) continue
205
+ s.overwrite(imp.s, imp.e, url)
206
+ edited = true
207
+ }
208
+
209
+ if (!edited) return code
210
+
211
+ let rewrittenCode = s.toString()
212
+ let rewriteMap = JSON.parse(s.generateMap({ hires: true }).toString())
213
+
214
+ let finalMap = tsToJsMap ? composeSourceMaps(rewriteMap, tsToJsMap) : rewriteMap
215
+ let mapJson = JSON.stringify(finalMap)
216
+ let mapBase64 = Buffer.from(mapJson).toString('base64')
217
+ return `${rewrittenCode}\n//# sourceMappingURL=data:application/json;base64,${mapBase64}`
218
+ }
219
+
220
+ // Strip the trailing `//# sourceMappingURL=data:application/json;base64,...`
221
+ // comment and decode the embedded JSON.
222
+ function extractInlineSourceMap(code: string): { code: string; map: unknown | null } {
223
+ let re =
224
+ /\n?\/\/# sourceMappingURL=data:application\/json(?:;charset=[^;,]+)?[;,]base64,([A-Za-z0-9+/=]+)\s*$/
225
+ let match = code.match(re)
226
+ if (!match || match.index == null) return { code, map: null }
227
+ try {
228
+ let decoded = Buffer.from(match[1], 'base64').toString('utf-8')
229
+ return { code: code.slice(0, match.index), map: JSON.parse(decoded) }
230
+ } catch {
231
+ return { code, map: null }
232
+ }
233
+ }
234
+
235
+ // Compose two source maps so positions in `secondMap`'s generated code map all
236
+ // the way back to `firstMap`'s original sources. `secondMap`'s "original" must
237
+ // be in the same coordinate space as `firstMap`'s "generated" — i.e. for our
238
+ // case the input to magic-string is the output of transformTypeScript.
239
+ function composeSourceMaps(secondMap: unknown, firstMap: unknown): unknown {
240
+ let secondConsumer = new SourceMapConsumer(secondMap as any)
241
+ let firstConsumer = new SourceMapConsumer(firstMap as any)
242
+ let gen = new SourceMapGenerator()
243
+
244
+ secondConsumer.eachMapping((mapping) => {
245
+ if (mapping.originalLine == null || mapping.originalColumn == null) return
246
+ let original = firstConsumer.originalPositionFor({
247
+ line: mapping.originalLine,
248
+ column: mapping.originalColumn,
249
+ })
250
+ if (original.line == null || original.column == null || original.source == null) return
251
+ gen.addMapping({
252
+ generated: { line: mapping.generatedLine, column: mapping.generatedColumn },
253
+ original: { line: original.line, column: original.column },
254
+ source: original.source,
255
+ name: original.name ?? mapping.name ?? undefined,
256
+ })
257
+ })
258
+
259
+ for (let source of firstConsumer.sources) {
260
+ let content = firstConsumer.sourceContentFor(source, true)
261
+ if (content !== null) gen.setSourceContent(source, content)
262
+ }
263
+
264
+ return gen.toJSON()
265
+ }
266
+
267
+ function resolveSpecifier(spec: string, importerFile: string, rootDir: string): string | null {
268
+ if (
269
+ spec.startsWith('node:') ||
270
+ spec.startsWith('http:') ||
271
+ spec.startsWith('https:') ||
272
+ spec.startsWith('data:') ||
273
+ spec.startsWith('/scripts/')
274
+ ) {
275
+ return null
276
+ }
277
+
278
+ let resolvedPath: string
279
+ if (spec.startsWith('.') || spec.startsWith('/')) {
280
+ resolvedPath = path.resolve(path.dirname(importerFile), spec)
281
+ } else {
282
+ // Bare specifiers must be resolved from the importer's filesystem
283
+ // location, not this module's. `import.meta.resolve(spec, parent)` looks
284
+ // like the right tool but its `parent` argument is gated behind
285
+ // `--experimental-import-meta-resolve` through at least Node 24 —
286
+ // without the flag, the parent argument is silently ignored and
287
+ // resolution happens from `import.meta.url` of the calling module. That
288
+ // made bare specifiers only resolvable when they were direct deps of
289
+ // `@remix-run/test` itself (so `remix/assert` failed even when the
290
+ // importing package depended on `remix`). `createRequire` walks
291
+ // node_modules from the importer's actual location and has been stable
292
+ // since Node 12 with no flags.
293
+ try {
294
+ resolvedPath = createRequire(importerFile).resolve(spec)
295
+ } catch {
296
+ return null
297
+ }
298
+ }
299
+
300
+ return filePathToUrl(resolvedPath, rootDir)
301
+ }
302
+
303
+ function sendHtml(res: http.ServerResponse, title: string, body: string): void {
304
+ let doc =
305
+ `<!DOCTYPE html>` +
306
+ `<html>` +
307
+ `<head>` +
308
+ `<meta charset="utf-8">` +
309
+ `<title>${escapeHtml(title)}</title>` +
310
+ `</head>` +
311
+ `<body>${body}</body>` +
312
+ `</html>`
313
+ res.writeHead(200, { 'Content-Type': 'text/html' })
314
+ res.end(doc)
315
+ }
316
+
317
+ function sendText(res: http.ServerResponse, status: number, body: string): void {
318
+ res.writeHead(status, { 'Content-Type': 'text/plain' })
319
+ res.end(body)
320
+ }
321
+
322
+ function escapeHtml(s: string): string {
323
+ return s
324
+ .replace(/&/g, '&amp;')
325
+ .replace(/</g, '&lt;')
326
+ .replace(/>/g, '&gt;')
327
+ .replace(/"/g, '&quot;')
328
+ }
329
+
330
+ // Prevent the embedded JSON from terminating the surrounding <script> element
331
+ // or being interpreted as HTML. Only `</` needs escaping inside an
332
+ // `application/json` block; the leading `<` is preserved as `<` in the
333
+ // emitted JSON so JSON.parse round-trips it unchanged.
334
+ function escapeJsonForScript(json: string): string {
335
+ return json.replace(/</g, '\\u003c')
336
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import * as process from 'node:process'
3
+
4
+ import { runRemixTest } from './cli.ts'
5
+
6
+ try {
7
+ let exitCode = await runRemixTest({
8
+ argv: process.argv.slice(2),
9
+ cwd: process.cwd(),
10
+ })
11
+ process.exit(exitCode)
12
+ } catch (error) {
13
+ console.error('Error running tests:', error)
14
+ process.exit(1)
15
+ }