@crab-dev/wake 0.1.21 → 0.1.22

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/index.cjs CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  const { EventEmitter } = require('node:events')
4
4
  const { loadNative } = require('./loader.cjs')
5
+ const {
6
+ registerTestContext,
7
+ setTestContextFatalError,
8
+ } = require('./test-context-internal.cjs')
5
9
  const {
6
10
  WakeError,
7
11
  fromNativeError,
@@ -25,6 +29,196 @@ async function bundle(options) {
25
29
  return invoke(native.bundle(JSON.stringify(value), signal))
26
30
  }
27
31
 
32
+ function splitTestOptions(options) {
33
+ try {
34
+ return splitOptions(options)
35
+ } catch (error) {
36
+ if (error instanceof WakeError && error.code === 'WAKE_CONFIG') {
37
+ throw new WakeError('WAKE_TEST_CONFIG', error.message, { cause: error })
38
+ }
39
+ throw error
40
+ }
41
+ }
42
+
43
+ async function runTests(options) {
44
+ const [value, signal] = splitTestOptions(options)
45
+ return invoke(native.runTests(
46
+ JSON.stringify(value),
47
+ signal,
48
+ native.__wakeTestHostPath,
49
+ ))
50
+ }
51
+
52
+ class TestContext extends EventEmitter {
53
+ #native
54
+ #closed = false
55
+ #eventPoll
56
+
57
+ constructor(handle) {
58
+ super()
59
+ this.#native = handle
60
+ registerTestContext(this, handle)
61
+ }
62
+
63
+ get closed() {
64
+ return this.#closed || this.#native.closed
65
+ }
66
+
67
+ get watching() {
68
+ return Boolean(this.#native.watching)
69
+ }
70
+
71
+ async run() {
72
+ if (this.closed) throw new WakeError('WAKE_TEST_CONTEXT', 'TestContext has already been closed')
73
+ try {
74
+ const result = await invoke(this.#native.run())
75
+ this.#flushEvents()
76
+ return result
77
+ } catch (error) {
78
+ this.#flushEvents()
79
+ throw error
80
+ }
81
+ }
82
+
83
+ startWatch() {
84
+ if (this.closed) throw new WakeError('WAKE_TEST_CONTEXT', 'TestContext has already been closed')
85
+ try {
86
+ this.#native.startWatch()
87
+ this.#flushEvents()
88
+ this.#startEventPoll()
89
+ return this
90
+ } catch (error) {
91
+ throw fromNativeError(error)
92
+ }
93
+ }
94
+
95
+ stopWatch() {
96
+ if (this.closed) return this
97
+ try {
98
+ this.#native.stopWatch()
99
+ this.#flushEvents()
100
+ this.#stopEventPoll()
101
+ return this
102
+ } catch (error) {
103
+ throw fromNativeError(error)
104
+ }
105
+ }
106
+
107
+ async close() {
108
+ if (this.#closed) return
109
+ this.#closed = true
110
+ this.#stopEventPoll()
111
+ try {
112
+ await invoke(this.#native.close())
113
+ } finally {
114
+ this.#flushEvents()
115
+ }
116
+ }
117
+
118
+ async [Symbol.asyncDispose]() {
119
+ await this.close()
120
+ }
121
+
122
+ #flushEvents() {
123
+ this.#dispatchEvents(this.#readEvents())
124
+ }
125
+
126
+ #readEvents() {
127
+ let events
128
+ try {
129
+ events = JSON.parse(this.#native.eventsJson())
130
+ } catch (cause) {
131
+ const detail = cause instanceof Error ? cause.message : String(cause)
132
+ throw new WakeError(
133
+ 'WAKE_TEST_HOST',
134
+ `Wake returned an invalid test event stream: ${detail}`,
135
+ { cause },
136
+ )
137
+ }
138
+ if (!Array.isArray(events)) {
139
+ throw new WakeError('WAKE_TEST_HOST', 'Wake returned a non-array test event stream')
140
+ }
141
+ return events
142
+ }
143
+
144
+ #dispatchEvents(events) {
145
+ for (const event of events) {
146
+ switch (event.type) {
147
+ case 'runStart':
148
+ this.emit('runStart', { runId: event.runId, watching: event.watching })
149
+ break
150
+ case 'testCaseResult':
151
+ this.emit('testCaseResult', {
152
+ runId: event.runId,
153
+ suiteId: event.suiteId,
154
+ result: event.result,
155
+ })
156
+ break
157
+ case 'suiteResult':
158
+ this.emit('suiteResult', {
159
+ runId: event.runId,
160
+ result: event.result,
161
+ })
162
+ break
163
+ case 'diagnostic':
164
+ this.emit('diagnostic', event.diagnostic)
165
+ break
166
+ case 'runComplete':
167
+ this.emit('runComplete', event.result)
168
+ break
169
+ case 'closed':
170
+ this.emit('closed')
171
+ break
172
+ default:
173
+ throw new WakeError('WAKE_TEST_HOST', `Unknown test-host event: ${String(event.type)}`)
174
+ }
175
+ }
176
+ }
177
+
178
+ #startEventPoll() {
179
+ if (this.#eventPoll) return
180
+ this.#eventPoll = setInterval(() => {
181
+ let events
182
+ try {
183
+ events = this.#readEvents()
184
+ } catch (error) {
185
+ this.#stopEventPoll()
186
+ const fatalError = error instanceof WakeError
187
+ ? error
188
+ : new WakeError('WAKE_TEST_HOST', error instanceof Error ? error.message : String(error), {
189
+ cause: error,
190
+ })
191
+ setTestContextFatalError(this, fatalError)
192
+ // Host terminals are drained as ordered native events before this error becomes visible;
193
+ // closing then supplies the final public `closed` event without fabricating a duplicate
194
+ // JavaScript diagnostic.
195
+ void this.close().catch(() => {})
196
+ return
197
+ }
198
+ // User listeners execute outside the native/protocol error boundary. Their exceptions are
199
+ // ordinary EventEmitter failures and must never be relabeled as WAKE_TEST_HOST.
200
+ this.#dispatchEvents(events)
201
+ }, 25)
202
+ }
203
+
204
+ #stopEventPoll() {
205
+ if (!this.#eventPoll) return
206
+ clearInterval(this.#eventPoll)
207
+ this.#eventPoll = undefined
208
+ }
209
+ }
210
+
211
+ async function createTestContext(options) {
212
+ const [value] = splitTestOptions(options)
213
+ try {
214
+ return new TestContext(
215
+ native.createTestContext(JSON.stringify(value), native.__wakeTestHostPath),
216
+ )
217
+ } catch (error) {
218
+ throw fromNativeError(error)
219
+ }
220
+ }
221
+
28
222
  async function buildLibrary(options) {
29
223
  const [value, signal] = splitOptions(options)
30
224
  return invoke(native.buildLibrary(JSON.stringify(value), signal))
@@ -208,14 +402,17 @@ function startDocsDevServer(options) {
208
402
  module.exports = {
209
403
  BuildContext,
210
404
  DevServer,
405
+ TestContext,
211
406
  WakeError,
212
407
  build,
213
408
  buildLibrary,
214
409
  buildDocs,
215
410
  bundle,
411
+ runTests,
216
412
  generateCssToken,
217
413
  generateDocgen,
218
414
  createBuildContext,
415
+ createTestContext,
219
416
  startDevServer,
220
417
  startDocsDevServer,
221
418
  version,
package/index.d.ts CHANGED
@@ -20,6 +20,23 @@ export type WakeErrorCode =
20
20
  | 'WAKE_LIBRARY_BUILD'
21
21
  | 'WAKE_LIBRARY_TYPE'
22
22
  | 'WAKE_LIBRARY_OUTPUT'
23
+ | 'WAKE_TEST_CONFIG'
24
+ | 'WAKE_TEST_DISCOVERY'
25
+ | 'WAKE_TEST_RUNTIME'
26
+ | 'WAKE_TEST_TIMEOUT'
27
+ | 'WAKE_TEST_SNAPSHOT'
28
+ | 'WAKE_TEST_COVERAGE'
29
+ | 'WAKE_TEST_HOST'
30
+ | 'WAKE_TEST_UNSUPPORTED'
31
+ | 'WAKE_TEST_CONTEXT'
32
+ | 'WAKE_TEST_DOM'
33
+ | 'WAKE_TEST_BROWSER'
34
+ | 'WAKE_TEST_REACT_VERSION'
35
+ | 'WAKE_TEST_NETWORK'
36
+ | 'WAKE_TEST_LEAK'
37
+ | 'WAKE_TEST_BUSY'
38
+ | 'WAKE_TEST_UNKNOWN_RUN'
39
+ | 'WAKE_TEST_UNKNOWN_WATCH'
23
40
 
24
41
  export interface DiagnosticLocation {
25
42
  /** One-based source line. */
@@ -66,6 +83,238 @@ export interface BuildOptions extends ProjectOptions {
66
83
  sourceMap?: boolean
67
84
  }
68
85
 
86
+ export type TestEnvironment = 'auto' | 'dom' | 'browser'
87
+ export type TestReporter = 'pretty' | 'json' | 'junit'
88
+ export type SnapshotUpdateMode = 'none' | 'new' | 'all'
89
+ export type TestWorkers = number | 'auto' | `${number}%`
90
+
91
+ export interface TestOptions {
92
+ root?: string
93
+ patterns?: string[]
94
+ namePattern?: string
95
+ projects?: string[]
96
+ environment?: TestEnvironment
97
+ watch?: boolean
98
+ changed?: boolean
99
+ related?: string[]
100
+ coverage?: boolean
101
+ updateSnapshots?: SnapshotUpdateMode
102
+ serial?: boolean
103
+ workers?: TestWorkers
104
+ bail?: number
105
+ shard?: `${number}/${number}`
106
+ seed?: string
107
+ shuffle?: boolean
108
+ reporter?: TestReporter
109
+ output?: string
110
+ allowNoTests?: boolean
111
+ browserPath?: string
112
+ headful?: boolean
113
+ }
114
+
115
+ export type TestCaseStatus = 'passed' | 'failed' | 'skipped' | 'todo'
116
+ export type TestSuiteStatus = 'passed' | 'failed' | 'skipped'
117
+ export type TestTerminationReason =
118
+ | 'completed'
119
+ | 'cancelled'
120
+ | 'bail'
121
+ | 'watch-restart'
122
+ | 'host-crash'
123
+ | 'timeout'
124
+ | 'oom'
125
+ | 'internal-error'
126
+
127
+ export interface TestLocation {
128
+ path: string
129
+ line: number
130
+ column: number
131
+ endLine: number | null
132
+ endColumn: number | null
133
+ }
134
+
135
+ export interface TestDiff {
136
+ expected: string | null
137
+ received: string | null
138
+ unified: string | null
139
+ }
140
+
141
+ export interface TestFailure {
142
+ message: string
143
+ code: string | null
144
+ stack: string | null
145
+ location: TestLocation | null
146
+ diff: TestDiff | null
147
+ }
148
+
149
+ export interface TestCaseResult {
150
+ id: string
151
+ name: string
152
+ fullName: string
153
+ status: TestCaseStatus
154
+ durationMs: number
155
+ assertions: number
156
+ attempts: number
157
+ location: TestLocation | null
158
+ failures: TestFailure[]
159
+ }
160
+
161
+ export interface SnapshotSummary {
162
+ added: number
163
+ matched: number
164
+ unmatched: number
165
+ updated: number
166
+ obsolete: number
167
+ filesRemoved: number
168
+ }
169
+
170
+ export interface CoverageMetric {
171
+ covered: number
172
+ total: number
173
+ percent: number
174
+ }
175
+
176
+ export interface CoverageMetrics {
177
+ lines: CoverageMetric
178
+ functions: CoverageMetric
179
+ blocks: CoverageMetric
180
+ }
181
+
182
+ export interface CoverageFile extends CoverageMetrics {
183
+ path: string
184
+ }
185
+
186
+ export interface CoverageResult {
187
+ summary: CoverageMetrics
188
+ files: CoverageFile[]
189
+ reportArtifactIds: string[]
190
+ }
191
+
192
+ export interface BrowserEnvironmentInfo {
193
+ name: string
194
+ version: string
195
+ headless: boolean
196
+ }
197
+
198
+ export interface TestEnvironmentInfo {
199
+ kind: 'dom' | 'browser'
200
+ react: string | null
201
+ reactDom: string | null
202
+ v8: string
203
+ browser: BrowserEnvironmentInfo | null
204
+ }
205
+
206
+ export type TestMetadataValue =
207
+ | null
208
+ | boolean
209
+ | number
210
+ | string
211
+ | TestMetadataValue[]
212
+ | { [key: string]: TestMetadataValue }
213
+
214
+ export interface TestArtifact {
215
+ id: string
216
+ kind: string
217
+ path: string
218
+ suiteId: string | null
219
+ testId: string | null
220
+ metadata: Record<string, TestMetadataValue>
221
+ }
222
+
223
+ export interface TestDiagnostic {
224
+ severity: 'error' | 'warning' | 'note' | 'help'
225
+ code: string
226
+ message: string
227
+ path: string | null
228
+ location: TestLocation | null
229
+ notes?: string[]
230
+ }
231
+
232
+ export interface TestLeak {
233
+ kind: 'timer' | 'listener' | 'task' | 'socket' | 'network' | 'other'
234
+ description: string
235
+ location: TestLocation | null
236
+ stack: string | null
237
+ }
238
+
239
+ export interface TestSuiteResult {
240
+ id: string
241
+ path: string
242
+ name: string | null
243
+ project: string | null
244
+ environment: TestEnvironmentInfo | null
245
+ status: TestSuiteStatus
246
+ durationMs: number
247
+ tests: TestCaseResult[]
248
+ failures: TestFailure[]
249
+ snapshot: SnapshotSummary | null
250
+ }
251
+
252
+ export interface TestStatusCounts {
253
+ total: number
254
+ passed: number
255
+ failed: number
256
+ skipped: number
257
+ }
258
+
259
+ export interface TestCaseStatusCounts extends TestStatusCounts {
260
+ todo: number
261
+ }
262
+
263
+ export interface TestRunCounts {
264
+ suites: TestStatusCounts
265
+ tests: TestCaseStatusCounts
266
+ }
267
+
268
+ export interface TestRunResult {
269
+ schemaVersion: 'wake.test.v1'
270
+ runId: string
271
+ success: boolean
272
+ seed: string
273
+ durationMs: number
274
+ terminationReason: TestTerminationReason
275
+ environment: TestEnvironmentInfo
276
+ suites: TestSuiteResult[]
277
+ counts: TestRunCounts
278
+ snapshot: SnapshotSummary
279
+ coverage: CoverageResult | null
280
+ leaks: TestLeak[]
281
+ artifacts: TestArtifact[]
282
+ diagnostics: TestDiagnostic[]
283
+ }
284
+
285
+ export interface TestRunStartEvent {
286
+ runId: string
287
+ watching: boolean
288
+ }
289
+
290
+ export interface TestCaseResultEvent {
291
+ runId: string
292
+ suiteId: string
293
+ result: TestCaseResult
294
+ }
295
+
296
+ export interface TestSuiteResultEvent {
297
+ runId: string
298
+ result: TestSuiteResult
299
+ }
300
+
301
+ export class TestContext extends EventEmitter {
302
+ private constructor()
303
+ readonly watching: boolean
304
+ readonly closed: boolean
305
+ run(): Promise<TestRunResult>
306
+ startWatch(): this
307
+ stopWatch(): this
308
+ close(): Promise<void>
309
+ [Symbol.asyncDispose](): Promise<void>
310
+ on(event: 'runStart', listener: (event: TestRunStartEvent) => void): this
311
+ on(event: 'testCaseResult', listener: (event: TestCaseResultEvent) => void): this
312
+ on(event: 'suiteResult', listener: (event: TestSuiteResultEvent) => void): this
313
+ on(event: 'runComplete', listener: (result: TestRunResult) => void): this
314
+ on(event: 'diagnostic', listener: (diagnostic: TestDiagnostic) => void): this
315
+ on(event: 'closed', listener: () => void): this
316
+ }
317
+
69
318
  export type BundlePlatform = 'browser' | 'node'
70
319
  export type BundleFormat = 'iife' | 'cjs'
71
320
  export type NodeTarget = `node${number}` | `node${number}.${number}`
@@ -272,6 +521,8 @@ export function buildLibrary(options?: LibraryBuildOptions): Promise<LibraryBuil
272
521
  export function generateCssToken(options?: GenerateCssTokenOptions): Promise<GenerateCssTokenResult>
273
522
  export function generateDocgen(options?: GenerateDocgenOptions): Promise<GenerateDocgenResult>
274
523
  export function createBuildContext(options?: BuildOptions): Promise<BuildContext>
524
+ export function runTests(options?: TestOptions & { signal?: AbortSignal }): Promise<TestRunResult>
525
+ export function createTestContext(options?: TestOptions): Promise<TestContext>
275
526
  export function startDevServer(options?: DevServerOptions): Promise<DevServer>
276
527
  export function buildDocs(options?: DocsBuildOptions): Promise<DocsBuildResult>
277
528
  export function startDocsDevServer(options?: DocsDevServerOptions): Promise<DevServer>
@@ -279,6 +530,7 @@ export function startDocsDevServer(options?: DocsDevServerOptions): Promise<DevS
279
530
  declare const wake: {
280
531
  BuildContext: typeof BuildContext
281
532
  DevServer: typeof DevServer
533
+ TestContext: typeof TestContext
282
534
  WakeError: typeof WakeError
283
535
  build: typeof build
284
536
  buildLibrary: typeof buildLibrary
@@ -287,6 +539,8 @@ declare const wake: {
287
539
  generateCssToken: typeof generateCssToken
288
540
  generateDocgen: typeof generateDocgen
289
541
  createBuildContext: typeof createBuildContext
542
+ runTests: typeof runTests
543
+ createTestContext: typeof createTestContext
290
544
  startDevServer: typeof startDevServer
291
545
  startDocsDevServer: typeof startDocsDevServer
292
546
  version: typeof version
package/index.mjs CHANGED
@@ -3,14 +3,17 @@ import api from './index.cjs'
3
3
  export const {
4
4
  BuildContext,
5
5
  DevServer,
6
+ TestContext,
6
7
  WakeError,
7
8
  build,
8
9
  buildLibrary,
9
10
  buildDocs,
10
11
  bundle,
12
+ runTests,
11
13
  generateCssToken,
12
14
  generateDocgen,
13
15
  createBuildContext,
16
+ createTestContext,
14
17
  startDevServer,
15
18
  startDocsDevServer,
16
19
  version,
package/loader.cjs CHANGED
@@ -17,9 +17,25 @@ function unsupported(message, cause) {
17
17
  return error
18
18
  }
19
19
 
20
+ function attachTestHost(native, nativePath) {
21
+ const hostPath = process.env.WAKE_TEST_HOST_PATH || path.join(
22
+ path.dirname(nativePath),
23
+ 'test-host',
24
+ process.platform === 'win32' ? 'wake-test-host.exe' : 'wake-test-host',
25
+ )
26
+ Object.defineProperty(native, '__wakeTestHostPath', {
27
+ configurable: false,
28
+ enumerable: false,
29
+ writable: false,
30
+ value: hostPath,
31
+ })
32
+ return native
33
+ }
34
+
20
35
  function loadNative() {
21
36
  if (process.env.WAKE_NATIVE_PATH) {
22
- return require(path.resolve(process.env.WAKE_NATIVE_PATH))
37
+ const nativePath = path.resolve(process.env.WAKE_NATIVE_PATH)
38
+ return attachTestHost(require(nativePath), nativePath)
23
39
  }
24
40
 
25
41
  const key = `${process.platform}-${process.arch}`
@@ -41,7 +57,8 @@ function loadNative() {
41
57
  }
42
58
 
43
59
  try {
44
- return require(packageName)
60
+ const nativePath = require.resolve(packageName)
61
+ return attachTestHost(require(nativePath), nativePath)
45
62
  } catch (cause) {
46
63
  throw unsupported(
47
64
  `Unable to load ${packageName} for ${process.platform}/${process.arch}. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crab-dev/wake",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Wake native web build tools for Node.js",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -22,6 +22,16 @@
22
22
  "import": "./experimental.mjs",
23
23
  "require": "./experimental.cjs"
24
24
  },
25
+ "./test": {
26
+ "types": "./test.d.ts",
27
+ "import": "./test.mjs",
28
+ "require": "./test.cjs"
29
+ },
30
+ "./test/react": {
31
+ "types": "./test-react.d.ts",
32
+ "import": "./test-react.mjs",
33
+ "require": "./test-react.cjs"
34
+ },
25
35
  "./internal/components-runtime": {
26
36
  "types": "./internal/components-runtime.d.ts",
27
37
  "import": "./internal/components-runtime.mjs"
@@ -41,6 +51,12 @@
41
51
  "*.mjs",
42
52
  "index.d.ts",
43
53
  "experimental.d.ts",
54
+ "test.cjs",
55
+ "test.mjs",
56
+ "test.d.ts",
57
+ "test-react.cjs",
58
+ "test-react.mjs",
59
+ "test-react.d.ts",
44
60
  "README.md",
45
61
  "CHANGELOG.md",
46
62
  "LICENSE-MIT",
@@ -50,7 +66,7 @@
50
66
  "node": ">=22.14 <27"
51
67
  },
52
68
  "dependencies": {
53
- "@crab-dev/css": "0.1.21",
69
+ "@crab-dev/css": "0.1.22",
54
70
  "@crab-dev/rc-alert": "^0.0.2",
55
71
  "@crab-dev/rc-button": "^0.0.2",
56
72
  "@crab-dev/rc-dialog": "^0.0.2",
@@ -71,15 +87,15 @@
71
87
  "string-width": "8.2.2"
72
88
  },
73
89
  "peerDependencies": {
74
- "react": "^19.2.8",
75
- "react-dom": "^19.2.8"
90
+ "react": ">=19.2.8 <19.3.0",
91
+ "react-dom": ">=19.2.8 <19.3.0"
76
92
  },
77
93
  "optionalDependencies": {
78
- "@crab-dev/wake-darwin-arm64": "0.1.21",
79
- "@crab-dev/wake-darwin-x64": "0.1.21",
80
- "@crab-dev/wake-linux-arm64-gnu": "0.1.21",
81
- "@crab-dev/wake-linux-x64-gnu": "0.1.21",
82
- "@crab-dev/wake-win32-x64-msvc": "0.1.21"
94
+ "@crab-dev/wake-darwin-arm64": "0.1.22",
95
+ "@crab-dev/wake-darwin-x64": "0.1.22",
96
+ "@crab-dev/wake-linux-arm64-gnu": "0.1.22",
97
+ "@crab-dev/wake-linux-x64-gnu": "0.1.22",
98
+ "@crab-dev/wake-win32-x64-msvc": "0.1.22"
83
99
  },
84
100
  "publishConfig": {
85
101
  "access": "public",
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const contexts = new WeakMap()
4
+
5
+ function registerTestContext(context, handle) {
6
+ contexts.set(context, { handle, fatalError: undefined })
7
+ }
8
+
9
+ function sendTestWatchControl(context, control) {
10
+ const record = contexts.get(context)
11
+ if (!record) throw new TypeError('Expected a Wake TestContext')
12
+ record.handle.watchControl(JSON.stringify(control))
13
+ }
14
+
15
+ function setTestContextFatalError(context, error) {
16
+ const record = contexts.get(context)
17
+ if (!record) throw new TypeError('Expected a Wake TestContext')
18
+ record.fatalError = error
19
+ }
20
+
21
+ function getTestContextFatalError(context) {
22
+ return contexts.get(context)?.fatalError
23
+ }
24
+
25
+ module.exports = {
26
+ getTestContextFatalError,
27
+ registerTestContext,
28
+ sendTestWatchControl,
29
+ setTestContextFatalError,
30
+ }