@stacksjs/testing 0.75.230 → 0.75.236

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/dist/browser.d.ts CHANGED
@@ -2,7 +2,15 @@
2
2
  * Poll `read` until it returns something truthy, and return that. Throws
3
3
  * after `timeoutMs` (default 12 s) with `label` in the message.
4
4
  */
5
- export declare function waitFor<T>(read: () => Promise<T> | T, timeoutMs?: unknown, options?: { intervalMs?: number, label?: string }): Promise<NonNullable<T>>;
5
+ export declare function waitFor<T>(read: () => Promise<T> | T, timeoutMs?: number, options?: { intervalMs?: number, label?: string }): Promise<NonNullable<T>>;
6
+ /**
7
+ * Whether `url` matches a `Fetch.enable` URL pattern the way Chrome matches
8
+ * it: the whole URL, `*` any run of characters (none included), `?` exactly
9
+ * one, a backslash makes the next character literal.
10
+ */
11
+ export declare function matchesUrlPattern(pattern: string, url: string): boolean;
12
+ /** Whether a `Fetch.requestPaused` event is one `pattern` would have paused. */
13
+ export declare function pausedRequestMatches(pattern: FetchPattern, paused: Pick<PausedRequest, 'request' | 'resourceType' | 'responseStatusCode' | 'responseErrorReason'>): boolean;
6
14
  /** Open a session on a DevTools WebSocket URL (`ws://127.0.0.1:9222/devtools/page/...`). */
7
15
  export declare function connect(url: string, options?: CdpOptions): Promise<Cdp>;
8
16
  /** The Chrome to drive: `CHROME_PATH`, else the first installed one found. Null when there is none. */
@@ -17,6 +25,15 @@ export declare function chromeHasUpdater(executable: string): boolean;
17
25
  * reused while its version matches the source's.
18
26
  */
19
27
  export declare function prepareMacChrome(options?: { source?: string, directory?: string }): PreparedChrome;
28
+ /**
29
+ * Answer live-reload streams matching `patterns` (default stx's) with one that
30
+ * asks to retry in a day, so a file saved mid-run cannot reload the page.
31
+ * Only those requests: every other paused request is left to the session's
32
+ * other Fetch handlers (`Cdp.intercept`, or your own `Fetch.enable`).
33
+ * `launch()` calls this unless `quietLiveReload` is false. Resolves with a
34
+ * function that stops it.
35
+ */
36
+ export declare function quietLiveReload(cdp: Cdp, patterns?: readonly string[]): Promise<() => Promise<void>>;
20
37
  /**
21
38
  * Start a private headless Chrome and open a session on its page, with the
22
39
  * `Page`, `Runtime`, `Network` and `Log` domains enabled.
@@ -33,11 +50,20 @@ export declare function settle(cdp: Cdp, options?: SettleOptions): Promise<boole
33
50
  export declare function navigate(cdp: Cdp, url: string, options?: SettleOptions): Promise<boolean>;
34
51
  /** Count how often each entry appears, keeping those seen more than once. */
35
52
  export declare function duplicateRequests(requests: readonly string[]): Record<string, number>;
53
+ /**
54
+ * Split a visit's requests into page loads (see `PageAuditOptions.pageLoads`).
55
+ * The first new document is the page itself; each later one, and each
56
+ * same-document navigation to another path, starts the next load.
57
+ */
58
+ export declare function splitPageLoads(url: string, events: readonly PageAuditEvent[], mode?: 'navigation' | 'visit'): PageLoad[];
59
+ /** The highest count each request reached in any one load. */
60
+ export declare function loadDuplicates(loads: readonly PageLoad[]): Record<string, number>;
36
61
  /**
37
62
  * Load `url` once and report what it cost and what went wrong: the same API
38
- * request made more than once in one load, failed same-origin requests, and
39
- * console errors. Never throws for a page problem: a launch or navigation
40
- * failure is reported in `errors`.
63
+ * request made more than once in one page load, failed same-origin requests,
64
+ * and console errors, and where the page landed. A page that sends you on
65
+ * (`/today` to `/home`) is counted as two loads (`pageLoads`). Never throws
66
+ * for a page problem: a launch or navigation failure is reported in `errors`.
41
67
  *
42
68
  * @example
43
69
  * const report = await auditPage('http://localhost:3000/dashboard', {
@@ -47,12 +73,31 @@ export declare function duplicateRequests(requests: readonly string[]): Record<s
47
73
  * expect(report.failed).toEqual([])
48
74
  */
49
75
  export declare function auditPage(url: string, options?: PageAuditOptions): Promise<PageAuditReport>;
50
- /** One line per page: `/path: 12 api | dup 2x GET /api/me | failed 404 /api/x | errors ...`. */
76
+ /** One line per page: `/today -> /home: 12 api | dup 2x GET /api/me | failed 404 /api/x | errors ...`. */
51
77
  export declare function formatPageAudit(report: PageAuditReport): string;
52
78
  /** True when the report found nothing: no duplicates, failures or errors, and the page settled. */
53
79
  export declare function pageAuditClean(report: PageAuditReport): boolean;
80
+ /** stx's live-reload stream. */
81
+ export declare const LIVE_RELOAD_PATTERNS: readonly string[];
54
82
  export declare interface CdpOptions {
55
83
  commandTimeoutMs?: number
84
+ connectTimeoutMs?: number
85
+ }
86
+ /** One `Fetch.enable` pattern, as the protocol spells it. */
87
+ export declare interface FetchPattern {
88
+ urlPattern?: string
89
+ resourceType?: string
90
+ requestStage?: 'Request' | 'Response'
91
+ }
92
+ /** The parts of a `Fetch.requestPaused` event a handler usually reads. */
93
+ export declare interface PausedRequest {
94
+ requestId: string
95
+ request: { url: string, method: string, headers: Record<string, string>, postData?: string }
96
+ resourceType?: string
97
+ responseStatusCode?: number
98
+ responseErrorReason?: string
99
+ networkId?: string
100
+ [key: string]: any
56
101
  }
57
102
  export declare interface PreparedChrome {
58
103
  executable: string
@@ -80,6 +125,22 @@ export declare interface LaunchOptions extends CdpOptions {
80
125
  userAgent?: boolean | string
81
126
  startTimeoutMs?: number
82
127
  }
128
+ export declare interface ChromeLaunchDiagnostics {
129
+ pid: number | null
130
+ chromePath: string
131
+ profile: string
132
+ temporaryProfile: boolean
133
+ elapsedMs: number
134
+ exitCodeBeforeCleanup: number | null
135
+ signalCodeBeforeCleanup: string | number | null
136
+ exitCode: number | null
137
+ signalCode: string | number | null
138
+ portFileExisted: boolean
139
+ portFileContents: string | null
140
+ stderrTail: string
141
+ stderrTruncated: boolean
142
+ cleanupError: string | null
143
+ }
83
144
  export declare interface LaunchedChrome {
84
145
  cdp: Cdp
85
146
  close: () => Promise<void>
@@ -103,11 +164,20 @@ export declare interface PageAuditOptions extends SettleOptions {
103
164
  viewport?: Viewport
104
165
  isApiRequest?: (url: URL, method: string) => boolean
105
166
  beforeNavigate?: (cdp: Cdp) => Promise<void> | void
167
+ pageLoads?: 'navigation' | 'visit'
168
+ }
169
+ /** The requests one page load made. */
170
+ export declare interface PageLoad {
171
+ url: string
172
+ apiRequests: string[]
173
+ duplicates: Record<string, number>
106
174
  }
107
175
  export declare interface PageAuditReport {
108
176
  url: string
177
+ finalUrl?: string
109
178
  apiRequests: string[]
110
179
  duplicates: Record<string, number>
180
+ loads?: PageLoad[]
111
181
  failed: string[]
112
182
  errors: string[]
113
183
  settled: boolean
@@ -117,6 +187,12 @@ export declare interface PageAuditReport {
117
187
  // The protocol session
118
188
  // ---------------------------------------------------------------------------
119
189
  declare type Listener = (params: any) => void;
190
+ /** Answers a paused request with `cdp.send('Fetch.continueRequest' | 'Fetch.fulfillRequest' | ...)`. */
191
+ export type FetchHandler = (request: PausedRequest, cdp: Cdp) => void | Promise<void>;
192
+ /** What happened during a visit, in order, for `splitPageLoads`. */
193
+ export type PageAuditEvent = | { type: 'request', request: string }
194
+ /** `sameDocument`: a history API or fragment navigation (`Page.navigatedWithinDocument`), not a new document. */
195
+ | { type: 'navigated', url: string, sameDocument: boolean }
120
196
  /**
121
197
  * One DevTools Protocol session.
122
198
  *
@@ -132,16 +208,42 @@ declare type Listener = (params: any) => void;
132
208
  *
133
209
  * Enable the `Network`, `Runtime` and `Log` domains for those to fill;
134
210
  * `launch()` does.
211
+ *
212
+ * ## Several Fetch handlers on one session
213
+ *
214
+ * Chrome keeps one set of `Fetch.enable` patterns per session: a second
215
+ * `Fetch.enable` replaces the first, `Fetch.disable` drops them all, and every
216
+ * paused request goes to every `Fetch.requestPaused` listener. So the session
217
+ * layers them:
218
+ *
219
+ * - `intercept(patterns, handler)` registers a handler that is given only the
220
+ * paused requests its own patterns match (`launch()` quiets live reload
221
+ * this way);
222
+ * - `send('Fetch.enable', { patterns })` and `send('Fetch.disable')` set and
223
+ * clear one more layer, for code that drives Fetch itself, and the session
224
+ * enables the union of every layer. `on('Fetch.requestPaused')` listeners
225
+ * are given the requests that layer's patterns match, and not those only an
226
+ * `intercept()` handler asked for;
227
+ * - the first answer to a paused request wins: a second
228
+ * `Fetch.continueRequest` / `fulfillRequest` / `failRequest` /
229
+ * `continueResponse` for it resolves without reaching Chrome (which would
230
+ * reject it), until the same request pauses again at its response stage;
231
+ * - a paused request nothing would answer is continued.
135
232
  */
136
233
  export declare class Cdp {
137
234
  errors: string[];
138
- inFlight: unknown;
235
+ inFlight: Map<string, string>;
139
236
  closed: boolean;
140
237
  constructor(socket: WebSocket, options?: CdpOptions);
238
+ intercept(patterns: FetchPattern[], handler: FetchHandler): Promise<() => Promise<void>>;
141
239
  on(method: string, listener: Listener): () => void;
142
240
  off(method: string, listener: Listener): void;
143
- once<T = any>(method: string, match?: (params: T) => boolean, timeoutMs?: unknown): Promise<T>;
241
+ once<T = any>(method: string, match?: (params: T) => boolean, timeoutMs?: number): Promise<T>;
144
242
  send<T = any>(method: string, params?: Record<string, unknown>): Promise<T>;
145
243
  evaluate<T = any>(expression: string): Promise<T>;
146
244
  disconnect(): void;
147
245
  }
246
+ export declare class ChromeLaunchError extends Error {
247
+ public readonly diagnostics: ChromeLaunchDiagnostics;
248
+ constructor(cause: unknown, diagnostics: ChromeLaunchDiagnostics);
249
+ }
package/dist/browser.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // @bun
2
- import{existsSync as b,mkdirSync as A,mkdtempSync as T,readFileSync as y,renameSync as q,rmSync as v}from"fs";import{tmpdir as R}from"os";import{join as p}from"path";import m from"process";async function x(e,t=12000,r={}){let n=Date.now(),i=await e();while(!i&&Date.now()-n<t)await Bun.sleep(r.intervalMs??100),i=await e();if(!i)throw Error(`Timed out after ${t}ms${r.label?` waiting for ${r.label}`:""}`);return i}class E{socket;id=0;pending=new Map;listeners=new Map;commandTimeoutMs;errors=[];inFlight=new Map;closed=!1;constructor(e,t={}){this.socket=e;this.commandTimeoutMs=t.commandTimeoutMs??30000,e.addEventListener("close",()=>{this.closed=!0;for(let[r,n]of this.pending)clearTimeout(n.timer),n.reject(Error("Chrome closed the connection")),this.pending.delete(r)}),e.addEventListener("message",(r)=>this.receive(String(r.data)))}receive(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id){let r=this.pending.get(t.id);if(!r)return;if(this.pending.delete(t.id),clearTimeout(r.timer),t.error)r.reject(Error(t.error.message));else r.resolve(t.result);return}this.track(t.method,t.params??{});for(let r of[...this.listeners.get(t.method)??[]])try{r(t.params??{})}catch(n){this.errors.push(`Listener for ${t.method} threw: ${n?.message??String(n)}`)}}track(e,t){if(e==="Network.requestWillBeSent")this.inFlight.set(t.requestId,`${t.request?.method} ${t.request?.url}`);else if(e==="Network.loadingFinished"||e==="Network.loadingFailed")this.inFlight.delete(t.requestId);else if(e==="Network.responseReceived"&&t.response?.mimeType==="text/event-stream")this.inFlight.delete(t.requestId);if(e==="Runtime.exceptionThrown")this.errors.push(t.exceptionDetails?.exception?.description||t.exceptionDetails?.text||"Runtime exception");else if(e==="Runtime.consoleAPICalled"&&t.type==="error")this.errors.push((t.args||[]).map((r)=>r.value??r.description??"").join(" "));else if(e==="Log.entryAdded"&&t.entry?.level==="error"&&!String(t.entry.text).includes("favicon"))this.errors.push(t.entry.text);else if(e==="Network.loadingFailed"&&!t.canceled)this.errors.push(`Network: ${t.errorText||"request failed"}`)}on(e,t){let r=this.listeners.get(e);if(!r)this.listeners.set(e,r=new Set);return r.add(t),()=>this.off(e,t)}off(e,t){this.listeners.get(e)?.delete(t)}once(e,t=()=>!0,r=this.commandTimeoutMs){return new Promise((n,i)=>{if(this.closed)return i(Error("Chrome closed the connection"));let a=setTimeout(()=>{c(),clearInterval(l),i(Error(`Timed out after ${r}ms waiting for ${e}`))},r),l=setInterval(()=>{if(!this.closed)return;clearTimeout(a),clearInterval(l),c(),i(Error("Chrome closed the connection"))},100),c=this.on(e,(u)=>{if(!t(u))return;clearTimeout(a),clearInterval(l),c(),n(u)})})}send(e,t={}){let r=++this.id;return new Promise((n,i)=>{if(this.closed)return i(Error("Chrome closed the connection"));let a=setTimeout(()=>{this.pending.delete(r),i(Error(`Chrome command timed out: ${e}`))},this.commandTimeoutMs);this.pending.set(r,{resolve:n,reject:i,timer:a}),this.socket.send(JSON.stringify({id:r,method:e,params:t}))})}async evaluate(e){let t=await this.send("Runtime.evaluate",{expression:e,awaitPromise:!0,returnByValue:!0});if(t.exceptionDetails)throw Error(t.exceptionDetails.exception?.description||t.exceptionDetails.text);return t.result?.value}disconnect(){try{this.socket.close()}catch{}}}async function D(e,t={}){let r=new WebSocket(e);return await new Promise((n,i)=>{r.addEventListener("open",()=>n(),{once:!0}),r.addEventListener("error",()=>i(Error(`Could not connect to Chrome at ${e}`)),{once:!0})}),new E(r,t)}var M=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"],L=["google-chrome","google-chrome-stable","chromium","chromium-browser","chrome"],k=["KSProductID","KSUpdateURL","KSChannelID","KSVersion"],P="dev.stacks.testing.chrome";function I(){if(m.env.CHROME_PATH)return m.env.CHROME_PATH;if(m.platform==="darwin"){let e=M.find((t)=>b(t));if(e)return e}for(let e of L){let t=Bun.which(e);if(t)return t}return null}function O(e){let t=/^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(e);return t?t[1]:null}function w(e,t){let r=Bun.spawnSync(["/usr/libexec/PlistBuddy","-c",t,e],{stdout:"pipe",stderr:"pipe"});return{ok:r.exitCode===0,out:r.stdout.toString().trim()}}function N(e){let t=O(e);if(!t||m.platform!=="darwin")return!1;let r=p(t,"Contents/Info.plist");return b(r)&&k.some((n)=>w(r,`Print :${n}`).ok)}function F(e={}){if(m.platform!=="darwin")throw Error("prepareMacChrome is for macOS; elsewhere launch Chrome as installed");let t=e.source??M[0],r=O(t);if(!r||!b(r))throw Error(`No Chrome app bundle at ${t}`);let n=p(r,"Contents/Info.plist"),i=w(n,"Print :CFBundleShortVersionString").out||"unknown",a=w(n,"Print :CFBundleExecutable").out||"Google Chrome",l=e.directory??p(R(),`stacks-chrome-${i.replace(/[^\w.-]/g,"_")}`),c=p(l,"Chrome.app"),u=p(c,"Contents/MacOS",a),h=p(c,"Contents/Info.plist");if(b(u)&&w(h,"Print :CFBundleIdentifier").out===P&&w(h,"Print :CFBundleShortVersionString").out===i)return{executable:u,app:c,reused:!0};A(l,{recursive:!0});let g=T(p(l,"staging-")),o=p(g,"Chrome.app"),f=(s,d)=>{let C=Bun.spawnSync(s,{stdout:"pipe",stderr:"pipe"});if(C.exitCode!==0)throw Error(`${d} failed: ${C.stderr.toString().trim()}`)};try{f(["cp","-cR",r,o],"Cloning Chrome");let s=p(o,"Contents/Info.plist");for(let d of k)w(s,`Delete :${d}`);f(["/usr/libexec/PlistBuddy","-c",`Set :CFBundleIdentifier ${P}`,s],"Renaming the Chrome bundle"),f(["codesign","--force","--deep","--sign","-",o],"Re-signing Chrome"),v(c,{recursive:!0,force:!0}),q(o,c)}finally{v(g,{recursive:!0,force:!0})}return{executable:u,app:c,reused:!1}}function B(e){let t=e.chromePath??m.env.CHROME_PATH;if(t)return t;let r=I();if(!r)throw Error("No Chrome found: install Google Chrome or Chromium, or set CHROME_PATH");if((e.stableMacChrome??!0)&&m.platform==="darwin"&&N(r))return F({source:r}).executable;return r}async function _(e={}){let t=B(e);if(!b(t))throw Error(`Chrome was not found at ${t}`);let r=e.profile||T(p(R(),"stacks-browser-"));v(p(r,"DevToolsActivePort"),{force:!0});let n=Bun.spawn([t,...e.headless===!1?[]:["--headless=new"],"--remote-debugging-port=0",`--user-data-dir=${r}`,"--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-extensions","--hide-scrollbars","--use-mock-keychain",...e.args??[],"about:blank"],{stdout:"ignore",stderr:"ignore"}),i,a=!1,l=async()=>{if(a)return;if(a=!0,i?.disconnect(),n.kill(),await n.exited,!e.profile)v(r,{recursive:!0,force:!0})};try{let c=p(r,"DevToolsActivePort");await x(()=>b(c)&&y(c,"utf8").includes(`
3
- `),e.startTimeoutMs??30000,{label:"Chrome to start"});let u=Number(y(c,"utf8").split(`
4
- `)[0]),h=await x(async()=>{let s=await fetch(`http://127.0.0.1:${u}/json/list`).then((d)=>d.json()).catch(()=>[]);return s.length?s:null},12000,{label:"a page target"}),g=h.find((s)=>s.type==="page"&&s.url==="about:blank")||h.find((s)=>s.type==="page");if(!g)throw Error("Chrome did not expose a page target");i=await D(g.webSocketDebuggerUrl,e);let o=i;if(await Promise.all([o.send("Page.enable"),o.send("Runtime.enable"),o.send("Network.enable"),o.send("Log.enable"),...e.viewport===!1?[]:[o.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,width:1440,height:1000,...e.viewport})]]),e.userAgent!==!1){let s=typeof e.userAgent==="string"?e.userAgent:String((await o.send("Browser.getVersion")).userAgent||"").replace("HeadlessChrome","Chrome");if(s)await o.send("Network.setUserAgentOverride",{userAgent:s})}let f=e.quietLiveReload===!1?[]:e.quietLiveReload??["*/_stx/hmr*"];if(f.length)o.on("Fetch.requestPaused",(s)=>{o.send("Fetch.fulfillRequest",{requestId:s.requestId,responseCode:200,responseHeaders:[{name:"Content-Type",value:"text/event-stream"}],body:Buffer.from(`retry: 86400000
2
+ import{closeSync as W,existsSync as E,mkdirSync as K,mkdtempSync as _,openSync as z,readSync as J,renameSync as Y,rmSync as F}from"fs";import{tmpdir as U}from"os";import{join as C}from"path";import x from"process";async function A(e,t=12000,r={}){let n=Date.now(),s=await e();while(!s&&Date.now()-n<t)await Bun.sleep(r.intervalMs??100),s=await e();if(!s)throw Error(`Timed out after ${t}ms${r.label?` waiting for ${r.label}`:""}`);return s}var Q=new Set(["Fetch.continueRequest","Fetch.fulfillRequest","Fetch.failRequest","Fetch.continueResponse"]);function X(e,t){let r="";for(let n=0;n<e.length;n++){let s=e[n];if(s==="\\"&&n+1<e.length)r+=D(e[++n]);else if(s==="*")r+="[\\s\\S]*";else if(s==="?")r+="[\\s\\S]";else r+=D(s)}return new RegExp(`^${r}$`).test(t)}function D(e){return e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&")}function I(e,t){let r=t.responseStatusCode!=null||t.responseErrorReason!=null?"Response":"Request";if((e.requestStage??"Request")!==r)return!1;if(e.resourceType&&e.resourceType!==t.resourceType)return!1;return X(e.urlPattern??"*",t.request?.url??"")}class H{socket;id=0;pending=new Map;listeners=new Map;commandTimeoutMs;interceptors=[];ownFetch=null;answered=new Map;errors=[];inFlight=new Map;closed=!1;constructor(e,t={}){this.socket=e;this.commandTimeoutMs=t.commandTimeoutMs??30000,e.addEventListener("close",()=>this.markClosed()),e.addEventListener("message",(r)=>this.receive(String(r.data)))}markClosed(){this.closed=!0;for(let[e,t]of this.pending)clearTimeout(t.timer),t.reject(Error("Chrome closed the connection")),this.pending.delete(e)}receive(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id){let r=this.pending.get(t.id);if(!r)return;if(this.pending.delete(t.id),clearTimeout(r.timer),t.error)r.reject(Error(t.error.message));else r.resolve(t.result);return}if(this.track(t.method,t.params??{}),t.method==="Fetch.requestPaused"&&!this.routePaused(t.params??{}))return;this.dispatch(t.method,t.params??{})}dispatch(e,t){for(let r of[...this.listeners.get(e)??[]])try{r(t)}catch(n){this.errors.push(`Listener for ${e} threw: ${n?.message??String(n)}`)}}routePaused(e){this.remember(e.requestId,!1);let t=this.interceptors.filter((s)=>s.patterns.some((o)=>I(o,e)));for(let s of t)try{let o=s.handler(e,this);if(o&&typeof o.catch==="function")o.catch((i)=>this.errors.push(`Fetch handler threw: ${i?.message??String(i)}`))}catch(o){this.errors.push(`Fetch handler threw: ${o?.message??String(o)}`)}let r=(this.listeners.get("Fetch.requestPaused")?.size??0)>0,n=this.ownFetch?this.ownFetch.patterns.some((s)=>I(s,e)):!1;if(!t.length&&!n&&!r)this.send("Fetch.continueRequest",{requestId:e.requestId}).catch(()=>{});return r&&(n||!t.length)}remember(e,t){if(this.answered.delete(e),this.answered.set(e,t),this.answered.size>2000){for(let[r]of this.answered)if(this.answered.delete(r),this.answered.size<=1000)break}}async applyFetch(){let e=[...this.interceptors.flatMap((t)=>t.patterns),...this.ownFetch?.patterns??[]];if(!e.length)return await this.command("Fetch.disable");return await this.command("Fetch.enable",{...this.ownFetch?.params,patterns:e})}async intercept(e,t){if(!e.length)throw TypeError("intercept needs at least one pattern");let r={patterns:e.map((s)=>({...s})),handler:t};this.interceptors.push(r);try{await this.applyFetch()}catch(s){throw this.interceptors=this.interceptors.filter((o)=>o!==r),s}let n=!1;return async()=>{if(n)return;if(n=!0,this.interceptors=this.interceptors.filter((s)=>s!==r),!this.closed)await this.applyFetch()}}track(e,t){if(e==="Network.requestWillBeSent")this.inFlight.set(t.requestId,`${t.request?.method} ${t.request?.url}`);else if(e==="Network.loadingFinished"||e==="Network.loadingFailed")this.inFlight.delete(t.requestId);else if(e==="Network.responseReceived"&&t.response?.mimeType==="text/event-stream")this.inFlight.delete(t.requestId);if(e==="Runtime.exceptionThrown")this.errors.push(t.exceptionDetails?.exception?.description||t.exceptionDetails?.text||"Runtime exception");else if(e==="Runtime.consoleAPICalled"&&t.type==="error")this.errors.push((t.args||[]).map((r)=>r.value??r.description??"").join(" "));else if(e==="Log.entryAdded"&&t.entry?.level==="error"&&!String(t.entry.text).includes("favicon"))this.errors.push(t.entry.text);else if(e==="Network.loadingFailed"&&!t.canceled)this.errors.push(`Network: ${t.errorText||"request failed"}`)}on(e,t){let r=this.listeners.get(e);if(!r)this.listeners.set(e,r=new Set);return r.add(t),()=>this.off(e,t)}off(e,t){this.listeners.get(e)?.delete(t)}once(e,t=()=>!0,r=this.commandTimeoutMs){return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let o=setTimeout(()=>{c(),clearInterval(i),s(Error(`Timed out after ${r}ms waiting for ${e}`))},r),i=setInterval(()=>{if(!this.closed)return;clearTimeout(o),clearInterval(i),c(),s(Error("Chrome closed the connection"))},100),c=this.on(e,(g)=>{if(!t(g))return;clearTimeout(o),clearInterval(i),c(),n(g)})})}send(e,t={}){if(e==="Fetch.enable"){let{patterns:r,...n}=t;return this.ownFetch={patterns:r?.length?r.map((s)=>({...s})):[{urlPattern:"*"}],params:n},this.closed?this.command(e,t):this.applyFetch()}if(e==="Fetch.disable")return this.ownFetch=null,this.closed?this.command(e,t):this.applyFetch();if(Q.has(e)&&typeof t.requestId==="string"){if(this.answered.get(t.requestId)===!0)return Promise.resolve({});if(this.answered.has(t.requestId))this.remember(t.requestId,!0)}return this.command(e,t)}command(e,t={}){let r=++this.id;return new Promise((n,s)=>{if(this.closed)return s(Error("Chrome closed the connection"));let o=setTimeout(()=>{this.pending.delete(r),s(Error(`Chrome command timed out: ${e}`))},this.commandTimeoutMs);this.pending.set(r,{resolve:n,reject:s,timer:o}),this.socket.send(JSON.stringify({id:r,method:e,params:t}))})}async evaluate(e){let t=await this.send("Runtime.evaluate",{expression:e,awaitPromise:!0,returnByValue:!0});if(t.exceptionDetails)throw Error(t.exceptionDetails.exception?.description||t.exceptionDetails.text);return t.result?.value}disconnect(){this.markClosed();try{this.socket.close()}catch{}}}async function Z(e,t={}){let r=new WebSocket(e);return await new Promise((n,s)=>{let o=(h)=>{if(clearTimeout(g),r.removeEventListener("open",i),r.removeEventListener("error",c),r.removeEventListener("close",c),h){try{r.close()}catch{}s(h)}else n()},i=()=>o(),c=()=>o(Error(`Could not connect to Chrome at ${e}`)),g=setTimeout(()=>o(Error(`Timed out connecting to Chrome at ${e}`)),t.connectTimeoutMs??30000);r.addEventListener("open",i,{once:!0}),r.addEventListener("error",c,{once:!0}),r.addEventListener("close",c,{once:!0})}),new H(r,t)}var j=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"],ee=["google-chrome","google-chrome-stable","chromium","chromium-browser","chrome"],G=["KSProductID","KSUpdateURL","KSChannelID","KSVersion"],O="dev.stacks.testing.chrome";function te(){if(x.env.CHROME_PATH)return x.env.CHROME_PATH;if(x.platform==="darwin"){let e=j.find((t)=>E(t));if(e)return e}for(let e of ee){let t=Bun.which(e);if(t)return t}return null}function V(e){let t=/^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(e);return t?t[1]:null}function S(e,t){let r=Bun.spawnSync(["/usr/libexec/PlistBuddy","-c",t,e],{stdout:"pipe",stderr:"pipe"});return{ok:r.exitCode===0,out:r.stdout.toString().trim()}}function re(e){let t=V(e);if(!t||x.platform!=="darwin")return!1;let r=C(t,"Contents/Info.plist");return E(r)&&G.some((n)=>S(r,`Print :${n}`).ok)}function ne(e={}){if(x.platform!=="darwin")throw Error("prepareMacChrome is for macOS; elsewhere launch Chrome as installed");let t=e.source??j[0],r=V(t);if(!r||!E(r))throw Error(`No Chrome app bundle at ${t}`);let n=C(r,"Contents/Info.plist"),s=S(n,"Print :CFBundleShortVersionString").out||"unknown",o=S(n,"Print :CFBundleExecutable").out||"Google Chrome",i=e.directory??C(U(),`stacks-chrome-${s.replace(/[^\w.-]/g,"_")}`),c=C(i,"Chrome.app"),g=C(c,"Contents/MacOS",o),h=C(c,"Contents/Info.plist");if(E(g)&&S(h,"Print :CFBundleIdentifier").out===O&&S(h,"Print :CFBundleShortVersionString").out===s)return{executable:g,app:c,reused:!0};K(i,{recursive:!0});let y=_(C(i,"staging-")),b=C(y,"Chrome.app"),P=(w,f)=>{let u=Bun.spawnSync(w,{stdout:"pipe",stderr:"pipe"});if(u.exitCode!==0)throw Error(`${f} failed: ${u.stderr.toString().trim()}`)};try{P(["cp","-cR",r,b],"Cloning Chrome");let w=C(b,"Contents/Info.plist");for(let f of G)S(w,`Delete :${f}`);P(["/usr/libexec/PlistBuddy","-c",`Set :CFBundleIdentifier ${O}`,w],"Renaming the Chrome bundle"),P(["codesign","--force","--deep","--sign","-",b],"Re-signing Chrome"),F(c,{recursive:!0,force:!0}),Y(b,c)}finally{F(y,{recursive:!0,force:!0})}return{executable:g,app:c,reused:!1}}class k extends Error{diagnostics;constructor(e,t){super(`${e instanceof Error?e.message:String(e)}
3
+ Chrome startup: ${JSON.stringify(t)}`,{cause:e});this.diagnostics=t;this.name="ChromeLaunchError"}}async function q(e,t){let r;try{return await Promise.race([e.then(()=>!0),new Promise((n)=>{r=setTimeout(()=>n(!1),t)})])}finally{clearTimeout(r)}}function M(e){let t;try{t=z(e,"r");let r=Buffer.alloc(1024);return r.subarray(0,J(t,r,0,r.length,0)).toString("utf8")}catch{return null}finally{if(t!==void 0)try{W(t)}catch{}}}var se=["*/_stx/hmr*"];async function oe(e,t=se){if(!t.length)return async()=>{};return await e.intercept(t.map((r)=>({urlPattern:r})),(r,n)=>{n.send("Fetch.fulfillRequest",{requestId:r.requestId,responseCode:200,responseHeaders:[{name:"Content-Type",value:"text/event-stream"}],body:Buffer.from(`retry: 86400000
5
4
 
6
- `).toString("base64")}).catch(()=>{})}),await o.send("Fetch.enable",{patterns:f.map((s)=>({urlPattern:s}))});if(e.acceptDialogs!==!1)o.on("Page.javascriptDialogOpening",(s)=>{o.errors.push(`Dialog (${s.type}) on ${s.url}: ${s.message||"(no message)"}`),o.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});return{cdp:o,close:l,target:g.webSocketDebuggerUrl,port:u,chromePath:t}}catch(c){throw await l(),c}}async function H(e,t={}){let r=t.quietMs??1000,n=t.timeoutMs??15000,i=Date.now(),a=0;while(Date.now()-i<n){if(e.closed)throw Error("Chrome closed the connection");if(e.inFlight.size>0||await e.evaluate("document.readyState !== 'complete'").catch(()=>!0))a=0;else if(!a)a=Date.now();else if(Date.now()-a>=r)return!0;await Bun.sleep(100)}return!1}async function U(e,t,r={}){let n=await e.send("Page.navigate",{url:t});if(n?.errorText)throw Error(`Could not open ${t}: ${n.errorText}`);return await H(e,r)}var j=(e)=>e.pathname.startsWith("/api/")&&!e.pathname.endsWith("/events");function S(e){let t={};for(let r of e)t[r]=(t[r]||0)+1;return Object.fromEntries(Object.entries(t).filter(([,r])=>r>1))}async function J(e,t={}){let r=Date.now(),n=new URL(e).origin,i=t.isApiRequest??j,a=[],l=[],c=null,u=t.cdp,h=[],g=u?.errors.length??0;try{if(!u)c=await _(t.launch),u=c.cdp;let o=u;if(h.push(o.on("Network.requestWillBeSent",(s)=>{let d=new URL(s.request.url);if(d.origin===n&&i(d,s.request.method))a.push(`${s.request.method} ${d.pathname}${d.search}`)})),h.push(o.on("Network.responseReceived",(s)=>{let d=new URL(s.response.url);if(d.origin===n&&s.response.status>=400)l.push(`${s.response.status} ${d.pathname}`)})),t.viewport)await o.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,...t.viewport});await t.beforeNavigate?.(o);let f=await U(o,e,t);return{url:e,apiRequests:a,duplicates:S(a),failed:l,errors:o.errors.slice(g),settled:f,durationMs:Date.now()-r}}catch(o){return{url:e,apiRequests:a,duplicates:S(a),failed:l,errors:[...u?.errors.slice(g)??[],`audit: ${o?.message??String(o)}`],settled:!1,durationMs:Date.now()-r}}finally{for(let o of h)o();await c?.close().catch(()=>{})}}function z(e){let t=e.url;try{let n=new URL(e.url);t=`${n.pathname}${n.search}`}catch{}let r=Object.entries(e.duplicates).map(([n,i])=>`${i}x ${n}`);return`${t}: ${e.apiRequests.length} api${r.length?` | dup ${r.join(", ")}`:""}${e.failed.length?` | failed ${e.failed.join(", ")}`:""}${e.errors.length?` | errors ${e.errors.slice(0,3).join(" ; ")}`:""}${e.settled?"":" | never settled"}`}function Y(e){return e.settled&&!Object.keys(e.duplicates).length&&!e.failed.length&&!e.errors.length}export{E as Cdp,J as auditPage,N as chromeHasUpdater,D as connect,S as duplicateRequests,I as findChrome,z as formatPageAudit,_ as launch,U as navigate,Y as pageAuditClean,F as prepareMacChrome,H as settle,x as waitFor};
5
+ `).toString("base64")}).catch(()=>{})})}function ie(e){let t=e.chromePath??x.env.CHROME_PATH;if(t)return t;let r=te();if(!r)throw Error("No Chrome found: install Google Chrome or Chromium, or set CHROME_PATH");if((e.stableMacChrome??!0)&&x.platform==="darwin"&&re(r))return ne({source:r}).executable;return r}async function ae(e={}){let t=e.startTimeoutMs??30000;if(!Number.isFinite(t)||t<=0)throw RangeError("startTimeoutMs must be positive and finite");let r=ie(e),n=Date.now();if(!E(r))throw Error(`Chrome was not found at ${r}`);let s=e.profile||_(C(U(),"stacks-browser-"));F(C(s,"DevToolsActivePort"),{force:!0});let o=C(s,"DevToolsActivePort"),i=(()=>{try{return Bun.spawn([r,...e.headless===!1?[]:["--headless=new"],"--remote-debugging-port=0",`--user-data-dir=${s}`,"--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-extensions","--hide-scrollbars","--use-mock-keychain",...e.args??[],"about:blank"],{stdin:"ignore",stdout:"ignore",stderr:"pipe"})}catch(p){let a=null;try{if(!e.profile)F(s,{recursive:!0,force:!0})}catch(l){a=String(l)}throw new k(p,{pid:null,chromePath:r,profile:s,temporaryProfile:!e.profile,elapsedMs:Date.now()-n,exitCodeBeforeCleanup:null,signalCodeBeforeCleanup:null,exitCode:null,signalCode:null,portFileExisted:!1,portFileContents:null,stderrTail:"",stderrTruncated:!1,cleanupError:a})}})(),c="",g=!1,h,y=Promise.resolve(),b,P,w=()=>P??=(async()=>{b?.disconnect();let p=!1,a=[];try{if(i.exitCode===null&&i.signalCode===null)try{i.kill("SIGTERM")}catch{}if(!await q(i.exited,2000)){try{i.kill("SIGKILL")}catch{}if(!await q(i.exited,2000))throw Error(`Chrome process ${i.pid} did not exit after SIGKILL`)}p=!0}catch(l){a.push(String(l)),p=i.exitCode!==null||i.signalCode!==null}finally{try{if(!await q(y,100)&&h){if(!await q(h.cancel(),100))throw Error("Chrome stderr cancellation did not finish")}if(!await q(y,100))throw Error("Chrome stderr reader did not finish")}catch(l){a.push(String(l))}finally{if(p&&!e.profile)try{F(s,{recursive:!0,force:!0})}catch(l){a.push(String(l))}}}if(a.length)throw Error(`Chrome cleanup failed: ${a.join("; ")}`)})(),f=()=>{let p=t-(Date.now()-n);if(p<=0)throw Error(`Timed out after ${t}ms starting Chrome`);if(i.exitCode!==null||i.signalCode!==null)throw Error("Chrome exited during startup");return p},u=setTimeout(()=>b?.disconnect(),Math.max(1,t-(Date.now()-n)));try{h=i.stderr.getReader();let p=h;y=(async()=>{let d=new TextDecoder;for(;;){let{value:T,done:R}=await p.read(),L=d.decode(T,{stream:!R});if(g||=c.length+L.length>16384,c=(c+L).slice(-16384),R)return}})().catch((d)=>{c=`${c}
6
+ [stderr read failed: ${String(d)}]`.slice(-16384)}),await A(()=>(f(),M(o)?.includes(`
7
+ `)),f(),{label:"Chrome to start"});let a=Number(M(o)?.split(`
8
+ `)[0]);if(!Number.isInteger(a)||a<1||a>65535)throw Error("Chrome wrote an invalid DevToolsActivePort");let l=await A(async()=>{let d=await fetch(`http://127.0.0.1:${a}/json/list`,{signal:AbortSignal.timeout(Math.min(1000,f()))}).then((T)=>T.json()).catch(()=>[]);return Array.isArray(d)&&d.length?d:null},Math.min(12000,f()),{label:"a page target"}),v=l.find((d)=>d.type==="page"&&d.url==="about:blank")||l.find((d)=>d.type==="page");if(!v)throw Error("Chrome did not expose a page target");b=await Z(v.webSocketDebuggerUrl,{...e,connectTimeoutMs:Math.min(e.connectTimeoutMs??30000,f())}),f();let m=b;if(await Promise.all([m.send("Page.enable"),m.send("Runtime.enable"),m.send("Network.enable"),m.send("Log.enable"),...e.viewport===!1?[]:[m.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,width:1440,height:1000,...e.viewport})]]),e.userAgent!==!1){let d=typeof e.userAgent==="string"?e.userAgent:String((await m.send("Browser.getVersion")).userAgent||"").replace("HeadlessChrome","Chrome");if(d)await m.send("Network.setUserAgentOverride",{userAgent:d})}if(e.quietLiveReload!==!1)await oe(m,e.quietLiveReload);if(e.acceptDialogs!==!1)m.on("Page.javascriptDialogOpening",(d)=>{m.errors.push(`Dialog (${d.type}) on ${d.url}: ${d.message||"(no message)"}`),m.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});return f(),clearTimeout(u),{cdp:m,close:w,target:v.webSocketDebuggerUrl,port:a,chromePath:r}}catch(p){clearTimeout(u);let a=Date.now()-n,{exitCode:l,signalCode:v}=i,m=E(o),d=M(o),T=null;try{await w()}catch(R){T=R instanceof Error?R.message:String(R)}throw new k(p,{pid:i.pid,chromePath:r,profile:s,temporaryProfile:!e.profile,elapsedMs:a,exitCodeBeforeCleanup:l,signalCodeBeforeCleanup:v,exitCode:i.exitCode,signalCode:i.signalCode,portFileExisted:m,portFileContents:d,stderrTail:c,stderrTruncated:g,cleanupError:T})}}async function ce(e,t={}){let r=t.quietMs??1000,n=t.timeoutMs??15000,s=Date.now(),o=0;while(Date.now()-s<n){if(e.closed)throw Error("Chrome closed the connection");if(e.inFlight.size>0||await e.evaluate("document.readyState !== 'complete'").catch(()=>!0))o=0;else if(!o)o=Date.now();else if(Date.now()-o>=r)return!0;await Bun.sleep(100)}return!1}async function le(e,t,r={}){let n=await e.send("Page.navigate",{url:t});if(n?.errorText)throw Error(`Could not open ${t}: ${n.errorText}`);return await ce(e,r)}var ue=(e)=>e.pathname.startsWith("/api/")&&!e.pathname.endsWith("/events");function de(e){let t={};for(let r of e)t[r]=(t[r]||0)+1;return Object.fromEntries(Object.entries(t).filter(([,r])=>r>1))}function N(e){try{let t=new URL(e);return`${t.origin}${t.pathname}`}catch{return e.replace(/[?#].*$/,"")}}function he(e,t,r="navigation"){let n=[{url:e,apiRequests:[]}],s=!1;for(let o of t){let i=n.at(-1);if(o.type==="request"){i.apiRequests.push(o.request);continue}let c=r==="navigation"&&s&&(o.sameDocument?N(o.url)!==N(i.url):!0);if(s=!0,c)n.push({url:o.url,apiRequests:[]});else i.url=o.url}return n.map((o)=>({...o,duplicates:de(o.apiRequests)}))}function pe(e){let t={};for(let r of e)for(let[n,s]of Object.entries(r.duplicates))t[n]=Math.max(t[n]??0,s);return t}async function ve(e,t={}){let r=Date.now(),n=new URL(e).origin,s=t.isApiRequest??ue,o=[],i=[],c=[],g=null,h=t.cdp,y=[],b=h?.errors.length??0,P,w=e,f=async(u,p=[])=>{let a=he(e,i,t.pageLoads),l=w;if(h&&!h.closed){let v=await h.evaluate("location.href").catch(()=>null);if(v&&v!=="about:blank"&&!v.startsWith("chrome-error:"))l=v}return{url:e,finalUrl:l,apiRequests:o,duplicates:pe(a),loads:a,failed:c,errors:[...h?.errors.slice(b)??[],...p],settled:u,durationMs:Date.now()-r}};try{if(!h)g=await ae(t.launch),h=g.cdp;let u=h;if(y.push(u.on("Network.requestWillBeSent",(a)=>{let l=new URL(a.request.url);if(l.origin===n&&s(l,a.request.method)){let v=`${a.request.method} ${l.pathname}${l.search}`;o.push(v),i.push({type:"request",request:v})}})),y.push(u.on("Network.responseReceived",(a)=>{let l=new URL(a.response.url);if(l.origin===n&&a.response.status>=400)c.push(`${a.response.status} ${l.pathname}`)})),y.push(u.on("Page.frameNavigated",(a)=>{if(a.frame?.parentId)return;P=a.frame?.id??P,w=a.frame?.unreachableUrl||`${a.frame?.url??w}${a.frame?.urlFragment??""}`,i.push({type:"navigated",url:w,sameDocument:!1})})),y.push(u.on("Page.navigatedWithinDocument",(a)=>{if(P&&a.frameId!==P)return;w=a.url,i.push({type:"navigated",url:a.url,sameDocument:!0})})),await u.send("Page.enable"),P=(await u.send("Page.getFrameTree").catch(()=>null))?.frameTree?.frame?.id,t.viewport)await u.send("Emulation.setDeviceMetricsOverride",{deviceScaleFactor:1,mobile:!1,...t.viewport});await t.beforeNavigate?.(u);let p=await le(u,e,t);return await f(p)}catch(u){return await f(!1,[`audit: ${u?.message??String(u)}`])}finally{for(let u of y)u();await g?.close().catch(()=>{})}}function B(e){try{let t=new URL(e);return`${t.pathname}${t.search}`}catch{return e}}function ye(e){let t=B(e.url),r=e.finalUrl?B(e.finalUrl):t,n=Object.entries(e.duplicates).map(([s,o])=>`${o}x ${s}`);return`${t}${r!==t?` -> ${r}`:""}: ${e.apiRequests.length} api${n.length?` | dup ${n.join(", ")}`:""}${e.failed.length?` | failed ${e.failed.join(", ")}`:""}${e.errors.length?` | errors ${e.errors.slice(0,3).join(" ; ")}`:""}${e.settled?"":" | never settled"}`}function Ce(e){return e.settled&&!Object.keys(e.duplicates).length&&!e.failed.length&&!e.errors.length}export{H as Cdp,k as ChromeLaunchError,se as LIVE_RELOAD_PATTERNS,ve as auditPage,re as chromeHasUpdater,Z as connect,de as duplicateRequests,te as findChrome,ye as formatPageAudit,ae as launch,pe as loadDuplicates,X as matchesUrlPattern,le as navigate,Ce as pageAuditClean,I as pausedRequestMatches,ne as prepareMacChrome,oe as quietLiveReload,ce as settle,he as splitPageLoads,A as waitFor};
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // @bun
2
- import D from"process";function pe(){D.env.NODE_ENV="test",D.env.APP_ENV="test"}var E=new Set(["headers","query","body","formData","actingAs"]);function L(e){if(!e)return"";let t=new URLSearchParams;for(let[r,o]of Object.entries(e)){if(o===void 0||o===null)continue;if(Array.isArray(o)){for(let a of o)t.append(r,String(a));continue}t.append(r,String(o))}let n=t.toString();return n?`?${n}`:""}function B(e,t,n){let r=Object.keys(e).filter((o)=>!E.has(o));if(r.length===0)return;throw TypeError(`http.${t.toLowerCase()}('${n}', \u2026) got unknown option(s) ${r.map((o)=>`'${o}'`).join(", ")}. Did you mean to send a body? Wrap it: { body: { ${r[0]}: \u2026 } }. `+`Valid options are ${[...E].join(", ")}.`)}async function j(){let e=await import("@stacksjs/router");return e.serverResponse}function U(e){let t={status:e.status,ok:e.ok,headers:e.headers,text:()=>e.text(),json:async()=>await e.clone().json(),assertStatus(n){if(this.status!==n)throw Error(`Expected status ${n}, got ${this.status}`);return t},async assertJson(n){let r=await e.clone().json();for(let[o,a]of Object.entries(n))if(JSON.stringify(r[o])!==JSON.stringify(a))throw Error(`assertJson: expected ${o}=${JSON.stringify(a)}, got ${JSON.stringify(r[o])}`);return t},assertHeader(n,r){let o=e.headers.get(n);if(o===null)throw Error(`assertHeader: missing header '${n}'`);if(r!==void 0){if(!(typeof r==="string"?o===r:r.test(o)))throw Error(`assertHeader: expected ${n}=${String(r)}, got '${o}'`)}return t}};return t}function x(e="http://localhost"){let t=null,n={},r=null;async function o(){if(!t)return null;if(r)return r;let{Auth:s}=await import("@stacksjs/auth"),u=Number(t.id);if(!Number.isFinite(u))throw TypeError(`actingAs() needs a user with a numeric id; got ${JSON.stringify(t.id)}`);let c=await s.loginUsingId(u);if(!c)throw Error(`actingAs() could not authenticate user ${u}: no such user. Create the row first (e.g. via a factory) so a token can be issued against it.`);return r=String(c.token),r}async function a(s){let u={Accept:"application/json",...n,...s.headers};if(s.body!==void 0&&s.formData===void 0)u["Content-Type"]="application/json";let c=await o();if(c&&!u.Authorization&&!u.authorization)u.Authorization=`Bearer ${c}`;return u}function l(s){if(s.formData!==void 0){if(s.body!==void 0)throw TypeError("A request carries either `body` or `formData`, not both.");let u=new FormData;for(let[c,p]of Object.entries(s.formData))u.append(c,p);return u}if(s.body===void 0)return;return typeof s.body==="string"?s.body:JSON.stringify(s.body)}async function d(s,u,c={}){let p=await j(),f=u.startsWith("http")?u:`${e}${u}`,y=L(c.query),I=y&&f.includes("?")?`${f}&${y.slice(1)}`:`${f}${y}`,_={method:s,headers:await a(c),body:l(c)},N=await p(new Request(I,_));return U(N)}let i={actingAs(s){return t=s,r=null,i},withHeaders(s){return n={...n,...s},i},request:(s,u,c)=>d(s,u,c),get:(s)=>d("GET",s),post:(s,u)=>d("POST",s,{body:u}),put:(s,u)=>d("PUT",s,{body:u}),patch:(s,u)=>d("PATCH",s,{body:u}),delete:(s,u)=>d("DELETE",s,{body:u})};return i}function fe(e,t){return x(t).actingAs(e)}var me=Object.freeze(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"].reduce((e,t)=>{let n=async(r,o={})=>{B(o,t,r);let{actingAs:a,...l}=o,d=x();if(a)d.actingAs(a);return d.request(t,r,l)};return{...e,[t.toLowerCase()]:n}},{}));import v from"process";var H=60000;function M(e){let t=[],n="",r=null,o=!1,a=!1;for(let l of e){if(o){n+=l,o=!1;continue}if(l==="\\"){o=!0,a=!0;continue}if(r){if(l===r)r=null;else n+=l;continue}if(l==='"'||l==="'"){r=l,a=!0;continue}if(/\s/.test(l)){if(a)t.push(n),n="",a=!1;continue}n+=l,a=!0}if(r)throw Error(`Unterminated ${r} in command: ${e}`);if(a)t.push(n);return t}function he(e){let t=typeof e==="string"?M(e):[...e],n,r={},o=v.cwd(),a=H;async function l(){if(t.length===0)throw Error("command() needs something to run");let i=Bun.spawn(["./buddy",...t],{cwd:o,env:{...v.env,...r},stdin:n===void 0?"ignore":new TextEncoder().encode(n),stdout:"pipe",stderr:"pipe"}),s=!1,u=setTimeout(()=>{s=!0,i.kill()},a);try{let[c,p,f]=await Promise.all([new Response(i.stdout).text(),new Response(i.stderr).text(),i.exited]);return{exitCode:f,stdout:c,stderr:p,output:c+p,timedOut:s}}finally{clearTimeout(u)}}let d={withInput(i){return n=i.map((s)=>`${s}
3
- `).join(""),d},withEnv(i){return r={...r,...i},d},withCwd(i){return o=i,d},withTimeout(i){if(!Number.isFinite(i)||i<=0)throw TypeError(`withTimeout expects a positive number of milliseconds, got ${i}`);return a=i,d},then(i,s){return l().then(i,s)}};return d}import{afterEach as J}from"bun:test";import{events as w}from"@stacksjs/events";var m=null,h=[],k=(e,t)=>{h.push({type:String(e),payload:t})};function Re(){if(m)return;m=[...w.all.entries()],w.all.clear(),h=[],w.on("*",k)}function De(){return m!==null}function G(e){if(!m)throw Error('getDispatchedEvents() was called without eventFake(). Nothing is recording, so an empty result would look like "the event did not fire".');return e===void 0?[...h]:h.filter((t)=>t.type===e)}function Ee(e){return G(e).length>0}function z(){if(!m)return;w.off("*",k),w.all.clear();for(let[e,t]of m)w.all.set(e,t);m=null,h=[]}J(()=>{z()});import{afterEach as Q}from"bun:test";import{CaptureEmailDriver as b,mail as O}from"@stacksjs/email";var g=null;function Oe(){if(g!==null)return;g=O.fake(),b.clear()}function Se(){return g!==null}function S(){if(g===null)throw Error('sentEmails() was called without mailFake(). Nothing is capturing, so an empty result would look like "no email was sent".');return[...b.all()]}function Ae(){return S().at(-1)}function Pe(e){let t=(n)=>{if(n===void 0||n===null)return!1;return(Array.isArray(n)?n:[n]).some((o)=>(typeof o==="string"?o:o?.address)===e)};return S().filter((n)=>t(n.to)||t(n.cc)||t(n.bcc))}function V(){if(g===null)return;O.restoreDriver(g),g=null,b.clear()}Q(()=>{V()});import{setSystemTime as A}from"bun:test";function K(e){let t=e instanceof Date?e:new Date(e);if(Number.isNaN(t.getTime()))throw TypeError(`Not a valid time: ${JSON.stringify(e)}`);return t}function W(e=new Date){let t=K(e);return A(t),t}function Fe(e){return W(e)}function qe(){A()}import{mkdtempSync as X,rmSync as Y,writeFileSync as P}from"fs";import{tmpdir as Z}from"os";import{join as T}from"path";import C from"process";var ee=2000;function te(e,t={}){let n=t.mode&&t.mode!=="auto"?t.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(e)?"test":"run";return[C.execPath,...t.config?[`--config=${t.config}`]:[],...t.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],e,...t.args??[]]}function ne(e){let t=e.inheritEnv===!1?{}:{...C.env};for(let[n,r]of Object.entries(e.env??{}))if(r===void 0)delete t[n];else t[n]=r;return Object.fromEntries(Object.entries(t).filter((n)=>n[1]!==void 0))}async function je(e,t={}){if(!e||typeof e!=="string")throw TypeError("runIsolated needs a file to run");let n=t.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");let r=null,o;if(t.preload){r=X(T(Z(),"stacks-isolated-"));let i=T(r,"noop-preload.ts");P(i,`export {}
4
- `);let s=JSON.stringify(t.preload.length?t.preload:[i]);o=T(r,"bunfig.toml"),P(o,`preload = ${s}
2
+ import D from"process";function me(){D.env.NODE_ENV="test",D.env.APP_ENV="test"}var x=new Set(["headers","query","body","formData","actingAs"]);function L(t){if(!t)return"";let e=new URLSearchParams;for(let[r,s]of Object.entries(t)){if(s===void 0||s===null)continue;if(Array.isArray(s)){for(let a of s)e.append(r,String(a));continue}e.append(r,String(s))}let n=e.toString();return n?`?${n}`:""}function B(t,e,n){let r=Object.keys(t).filter((s)=>!x.has(s));if(r.length===0)return;throw TypeError(`http.${e.toLowerCase()}('${n}', \u2026) got unknown option(s) ${r.map((s)=>`'${s}'`).join(", ")}. Did you mean to send a body? Wrap it: { body: { ${r[0]}: \u2026 } }. `+`Valid options are ${[...x].join(", ")}.`)}async function j(){let t=await import("@stacksjs/router");return t.serverResponse}function U(t){let e={status:t.status,ok:t.ok,headers:t.headers,text:()=>t.text(),json:async()=>await t.clone().json(),assertStatus(n){if(this.status!==n)throw Error(`Expected status ${n}, got ${this.status}`);return e},async assertJson(n){let r=await t.clone().json();for(let[s,a]of Object.entries(n))if(JSON.stringify(r[s])!==JSON.stringify(a))throw Error(`assertJson: expected ${s}=${JSON.stringify(a)}, got ${JSON.stringify(r[s])}`);return e},assertHeader(n,r){let s=t.headers.get(n);if(s===null)throw Error(`assertHeader: missing header '${n}'`);if(r!==void 0){if(!(typeof r==="string"?s===r:r.test(s)))throw Error(`assertHeader: expected ${n}=${String(r)}, got '${s}'`)}return e}};return e}function v(t="http://localhost"){let e=null,n={},r=null;async function s(){if(!e)return null;if(r)return r;let{Auth:o}=await import("@stacksjs/auth"),u=Number(e.id);if(!Number.isFinite(u))throw TypeError(`actingAs() needs a user with a numeric id; got ${JSON.stringify(e.id)}`);let c=await o.loginUsingId(u);if(!c)throw Error(`actingAs() could not authenticate user ${u}: no such user. Create the row first (e.g. via a factory) so a token can be issued against it.`);return r=String(c.token),r}async function a(o){let u={Accept:"application/json",...n,...o.headers};if(o.body!==void 0&&o.formData===void 0)u["Content-Type"]="application/json";let c=await s();if(c&&!u.Authorization&&!u.authorization)u.Authorization=`Bearer ${c}`;return u}function l(o){if(o.formData!==void 0){if(o.body!==void 0)throw TypeError("A request carries either `body` or `formData`, not both.");let u=new FormData;for(let[c,p]of Object.entries(o.formData))u.append(c,p);return u}if(o.body===void 0)return;return typeof o.body==="string"?o.body:JSON.stringify(o.body)}async function d(o,u,c={}){let p=await j(),f=u.startsWith("http")?u:`${t}${u}`,b=L(c.query),I=b&&f.includes("?")?`${f}&${b.slice(1)}`:`${f}${b}`,_={method:o,headers:await a(c),body:l(c)},N=await p(new Request(I,_));return U(N)}let i={actingAs(o){return e=o,r=null,i},withHeaders(o){return n={...n,...o},i},request:(o,u,c)=>d(o,u,c),get:(o)=>d("GET",o),post:(o,u)=>d("POST",o,{body:u}),put:(o,u)=>d("PUT",o,{body:u}),patch:(o,u)=>d("PATCH",o,{body:u}),delete:(o,u)=>d("DELETE",o,{body:u})};return i}function ge(t,e){return v(e).actingAs(t)}var we=Object.freeze(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"].reduce((t,e)=>{let n=async(r,s={})=>{B(s,e,r);let{actingAs:a,...l}=s,d=v();if(a)d.actingAs(a);return d.request(e,r,l)};return{...t,[e.toLowerCase()]:n}},{}));import k from"process";var H=60000;function M(t){let e=[],n="",r=null,s=!1,a=!1;for(let l of t){if(s){n+=l,s=!1;continue}if(l==="\\"){s=!0,a=!0;continue}if(r){if(l===r)r=null;else n+=l;continue}if(l==='"'||l==="'"){r=l,a=!0;continue}if(/\s/.test(l)){if(a)e.push(n),n="",a=!1;continue}n+=l,a=!0}if(r)throw Error(`Unterminated ${r} in command: ${t}`);if(a)e.push(n);return e}function ye(t){let e=typeof t==="string"?M(t):[...t],n,r={},s=k.cwd(),a=H;async function l(){if(e.length===0)throw Error("command() needs something to run");let i=Bun.spawn(["./buddy",...e],{cwd:s,env:{...k.env,...r},stdin:n===void 0?"ignore":new TextEncoder().encode(n),stdout:"pipe",stderr:"pipe"}),o=!1,u=setTimeout(()=>{o=!0,i.kill()},a);try{let[c,p,f]=await Promise.all([new Response(i.stdout).text(),new Response(i.stderr).text(),i.exited]);return{exitCode:f,stdout:c,stderr:p,output:c+p,timedOut:o}}finally{clearTimeout(u)}}let d={withInput(i){return n=i.map((o)=>`${o}
3
+ `).join(""),d},withEnv(i){return r={...r,...i},d},withCwd(i){return s=i,d},withTimeout(i){if(!Number.isFinite(i)||i<=0)throw TypeError(`withTimeout expects a positive number of milliseconds, got ${i}`);return a=i,d},then(i,o){return l().then(i,o)}};return d}import{afterEach as J}from"bun:test";import{events as w}from"@stacksjs/events";var m=null,h=[],O=(t,e)=>{h.push({type:String(t),payload:e})};function De(){if(m)return;m=[...w.all.entries()],w.all.clear(),h=[],w.on("*",O)}function xe(){return m!==null}function G(t){if(!m)throw Error('getDispatchedEvents() was called without eventFake(). Nothing is recording, so an empty result would look like "the event did not fire".');return t===void 0?[...h]:h.filter((e)=>e.type===t)}function ve(t){return G(t).length>0}function z(){if(!m)return;w.off("*",O),w.all.clear();for(let[t,e]of m)w.all.set(t,e);m=null,h=[]}J(()=>{z()});import{afterEach as Q}from"bun:test";import{CaptureEmailDriver as y,mail as S}from"@stacksjs/email";var g=null;function Ae(){if(g!==null)return;g=S.fake(),y.clear()}function Pe(){return g!==null}function A(){if(g===null)throw Error('sentEmails() was called without mailFake(). Nothing is capturing, so an empty result would look like "no email was sent".');return[...y.all()]}function Ce(){return A().at(-1)}function $e(t){let e=(n)=>{if(n===void 0||n===null)return!1;return(Array.isArray(n)?n:[n]).some((s)=>(typeof s==="string"?s:s?.address)===t)};return A().filter((n)=>e(n.to)||e(n.cc)||e(n.bcc))}function V(){if(g===null)return;S.restoreDriver(g),g=null,y.clear()}Q(()=>{V()});import{setSystemTime as P}from"bun:test";function K(t){let e=t instanceof Date?t:new Date(t);if(Number.isNaN(e.getTime()))throw TypeError(`Not a valid time: ${JSON.stringify(t)}`);return e}function W(t=new Date){let e=K(t);return P(e),e}function Ie(t){return W(t)}function _e(){P()}import{existsSync as X,mkdtempSync as Y,rmSync as Z,writeFileSync as C}from"fs";import{tmpdir as ee}from"os";import{join as T,resolve as te}from"path";import R from"process";var ne=2000;function re(t,e={}){let n=e.mode&&e.mode!=="auto"?e.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(t)?"test":"run";return[R.execPath,...e.config?[`--config=${e.config}`]:[],...e.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],t,...e.args??[]]}function se(t){let e=t.inheritEnv===!1?{}:{...R.env};for(let[n,r]of Object.entries(t.env??{}))if(r===void 0)delete e[n];else e[n]=r;return Object.fromEntries(Object.entries(e).filter((n)=>n[1]!==void 0))}async function He(t,e={}){if(!t||typeof t!=="string")throw TypeError("runIsolated needs a file to run");let n=e.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");if(e.bunfig!=null&&e.preload)throw TypeError("runIsolated takes bunfig or preload, not both: list the preloads in that bunfig");let r=null,s;if(e.bunfig!=null){if(!e.bunfig)throw TypeError("bunfig must be a path to a bunfig.toml");if(s=te(e.cwd??R.cwd(),e.bunfig),!X(s))throw Error(`runIsolated: no bunfig at ${s}`)}if(e.preload){r=Y(T(ee(),"stacks-isolated-"));let i=T(r,"noop-preload.ts");C(i,`export {}
4
+ `);let o=JSON.stringify(e.preload.length?e.preload:[i]);s=T(r,"bunfig.toml"),C(s,`preload = ${o}
5
5
 
6
6
  [test]
7
- preload = ${s}
8
- `)}let a=te(e,{...t,config:o}),l=Date.now(),d=!1;try{let i=Bun.spawn(a,{cwd:t.cwd,env:ne(t),stdin:t.stdin!=null?new Blob([t.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),s,u=setTimeout(()=>{d=!0,i.kill("SIGTERM"),s=setTimeout(()=>i.kill("SIGKILL"),ee)},n);try{let[c,p,f]=await Promise.all([i.exited,new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:i.signalCode?null:c,signal:i.signalCode==null?null:String(i.signalCode),stdout:p,stderr:f,timedOut:d,durationMs:Date.now()-l,command:a}}finally{if(clearTimeout(u),clearTimeout(s),i.exitCode===null&&!i.signalCode)i.kill("SIGKILL")}}finally{if(r)Y(r,{recursive:!0,force:!0})}}function Ue(e,t){let n=`
7
+ preload = ${o}
8
+ `)}let a=re(t,{...e,config:s}),l=Date.now(),d=!1;try{let i=Bun.spawn(a,{cwd:e.cwd,env:se(e),stdin:e.stdin!=null?new Blob([e.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),o,u=setTimeout(()=>{d=!0,i.kill("SIGTERM"),o=setTimeout(()=>i.kill("SIGKILL"),ne)},n);try{let[c,p,f]=await Promise.all([i.exited,new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:i.signalCode?null:c,signal:i.signalCode==null?null:String(i.signalCode),stdout:p,stderr:f,timedOut:d,durationMs:Date.now()-l,command:a}}finally{if(clearTimeout(u),clearTimeout(o),i.exitCode===null&&!i.signalCode)i.kill("SIGKILL")}}finally{if(r)Z(r,{recursive:!0,force:!0})}}function Me(t,e){let n=`
9
9
  --- stdout ---
10
- ${e.stdout}
10
+ ${t.stdout}
11
11
  --- stderr ---
12
- ${e.stderr}`;if(e.timedOut)throw Error(`${e.command.slice(1).join(" ")} timed out after ${e.durationMs}ms${n}`);if(e.code!==0)throw Error(`${e.command.slice(1).join(" ")} exited ${e.code??e.signal}${n}`);if(t!=null&&!(typeof t==="string"?e.stdout.includes(t):t.test(e.stdout)))throw Error(`${e.command.slice(1).join(" ")} did not print ${String(t)}${n}`);return e}import{mkdtempSync as re,rmSync as R}from"fs";import{tmpdir as se}from"os";import{join as F}from"path";import oe from"process";var{SQL:q}=globalThis.Bun;var ie=new Set(["localhost","127.0.0.1","[::1]","::1"]);function ae(e={}){return e.url||oe.env.STACKS_TEST_POSTGRES_URL||void 0}function ue(e){let t;try{t=new URL(e)}catch{throw Error("A disposable Postgres database needs a postgres:// URL")}if(!["postgres:","postgresql:"].includes(t.protocol))throw Error(`A disposable Postgres database needs a postgres:// URL, got ${t.protocol}`);if(!ie.has(t.hostname))throw Error(`Disposable databases are only created on a local PostgreSQL server (localhost, 127.0.0.1, ::1), not "${t.hostname}"`);return t}function le(e){return`${(e.toLowerCase().replace(/[^a-z0-9_]/g,"_").replace(/^[^a-z_]+/,"")||"stacks_test").slice(0,38)}_${crypto.randomUUID().replaceAll("-","").slice(0,24)}`}async function de(e,t={}){if(e!=="sqlite"&&e!=="postgres")throw Error(`Disposable databases are sqlite or postgres, not "${String(e)}"`);let n=le(t.prefix??"stacks_test"),r=re(F(se(),`${n.slice(0,20)}-`)),o=!1;if(e==="sqlite"){let a=F(r,`${n}.sqlite`),l=async()=>{if(o)return;o=!0,R(r,{recursive:!0,force:!0})};return{dialect:e,name:n,directory:r,path:a,env:{DB_CONNECTION:"sqlite",DB_DATABASE_PATH:a},drop:l,[Symbol.asyncDispose]:l}}try{let a=ae(t);if(!a)throw Error("A disposable Postgres database needs a server: pass options.url or set STACKS_TEST_POSTGRES_URL");let l=ue(a),d=new q(l.href);try{await d.unsafe(`CREATE DATABASE "${n}"`)}finally{await d.close()}let i=new URL(l.href);i.pathname=`/${n}`;let s=async()=>{if(o)return;o=!0;let u=new q(l.href);try{await u.unsafe(`DROP DATABASE IF EXISTS "${n}" WITH (FORCE)`)}finally{await u.close(),R(r,{recursive:!0,force:!0})}};return{dialect:e,name:n,directory:r,url:i.href,env:{DB_CONNECTION:"postgres",DB_HOST:l.hostname.replace(/^\[|\]$/g,""),DB_PORT:l.port||"5432",DB_DATABASE:n,DB_USERNAME:decodeURIComponent(l.username),DB_PASSWORD:decodeURIComponent(l.password)},drop:s,[Symbol.asyncDispose]:s}}catch(a){throw R(r,{recursive:!0,force:!0}),a}}async function Ve(e,t,n={}){let r=await de(e,n),o;try{o=await t(r)}catch(a){throw await r.drop().catch(()=>{}),a}return await r.drop(),o}export*from"bun:test";export{fe as actingAs,Ue as assertIsolatedOk,ue as assertLocalPostgres,B as assertRequestOptions,he as command,de as createDisposableDatabase,ae as disposablePostgresUrl,Pe as emailsTo,Re as eventFake,De as eventsAreFaked,x as featureTest,W as freezeTime,G as getDispatchedEvents,Ee as hasDispatchedEvent,me as http,te as isolatedCommand,Ae as lastEmail,Oe as mailFake,Se as mailIsFaked,L as queryString,z as restoreEvents,V as restoreMail,je as runIsolated,S as sentEmails,pe as setupTestEnvironment,M as splitCommand,Fe as travelTo,qe as useRealTime,Ve as withDisposableDatabase};
12
+ ${t.stderr}`;if(t.timedOut)throw Error(`${t.command.slice(1).join(" ")} timed out after ${t.durationMs}ms${n}`);if(t.code!==0)throw Error(`${t.command.slice(1).join(" ")} exited ${t.code??t.signal}${n}`);if(e!=null&&!(typeof e==="string"?t.stdout.includes(e):e.test(t.stdout)))throw Error(`${t.command.slice(1).join(" ")} did not print ${String(e)}${n}`);return t}import{mkdtempSync as oe,rmSync as E}from"fs";import{tmpdir as ie}from"os";import{join as F}from"path";import ae from"process";var{SQL:q}=globalThis.Bun;var ue=new Set(["localhost","127.0.0.1","[::1]","::1"]);function le(t={}){return t.url||ae.env.STACKS_TEST_POSTGRES_URL||void 0}function de(t){let e;try{e=new URL(t)}catch{throw Error("A disposable Postgres database needs a postgres:// URL")}if(!["postgres:","postgresql:"].includes(e.protocol))throw Error(`A disposable Postgres database needs a postgres:// URL, got ${e.protocol}`);if(!ue.has(e.hostname))throw Error(`Disposable databases are only created on a local PostgreSQL server (localhost, 127.0.0.1, ::1), not "${e.hostname}"`);return e}function ce(t){return`${(t.toLowerCase().replace(/[^a-z0-9_]/g,"_").replace(/^[^a-z_]+/,"")||"stacks_test").slice(0,38)}_${crypto.randomUUID().replaceAll("-","").slice(0,24)}`}async function pe(t,e={}){if(t!=="sqlite"&&t!=="postgres")throw Error(`Disposable databases are sqlite or postgres, not "${String(t)}"`);let n=ce(e.prefix??"stacks_test"),r=oe(F(ie(),`${n.slice(0,20)}-`)),s=!1;if(t==="sqlite"){let a=F(r,`${n}.sqlite`),l=async()=>{if(s)return;s=!0,E(r,{recursive:!0,force:!0})};return{dialect:t,name:n,directory:r,path:a,env:{DB_CONNECTION:"sqlite",DB_DATABASE_PATH:a},drop:l,[Symbol.asyncDispose]:l}}try{let a=le(e);if(!a)throw Error("A disposable Postgres database needs a server: pass options.url or set STACKS_TEST_POSTGRES_URL");let l=de(a),d=new q(l.href);try{await d.unsafe(`CREATE DATABASE "${n}"`)}finally{await d.close()}let i=new URL(l.href);i.pathname=`/${n}`;let o=async()=>{if(s)return;s=!0;let u=new q(l.href);try{await u.unsafe(`DROP DATABASE IF EXISTS "${n}" WITH (FORCE)`)}finally{await u.close(),E(r,{recursive:!0,force:!0})}};return{dialect:t,name:n,directory:r,url:i.href,env:{DB_CONNECTION:"postgres",DB_HOST:l.hostname.replace(/^\[|\]$/g,""),DB_PORT:l.port||"5432",DB_DATABASE:n,DB_USERNAME:decodeURIComponent(l.username),DB_PASSWORD:decodeURIComponent(l.password)},drop:o,[Symbol.asyncDispose]:o}}catch(a){throw E(r,{recursive:!0,force:!0}),a}}async function We(t,e,n={}){let r=await pe(t,n),s;try{s=await e(r)}catch(a){throw await r.drop().catch(()=>{}),a}return await r.drop(),s}export*from"bun:test";export{ge as actingAs,Me as assertIsolatedOk,de as assertLocalPostgres,B as assertRequestOptions,ye as command,pe as createDisposableDatabase,le as disposablePostgresUrl,$e as emailsTo,De as eventFake,xe as eventsAreFaked,v as featureTest,W as freezeTime,G as getDispatchedEvents,ve as hasDispatchedEvent,we as http,re as isolatedCommand,Ce as lastEmail,Ae as mailFake,Pe as mailIsFaked,L as queryString,z as restoreEvents,V as restoreMail,He as runIsolated,A as sentEmails,me as setupTestEnvironment,M as splitCommand,Ie as travelTo,_e as useRealTime,We as withDisposableDatabase};
@@ -27,6 +27,7 @@ export declare interface RunIsolatedOptions {
27
27
  cwd?: string
28
28
  mode?: 'auto' | 'test' | 'run'
29
29
  preload?: string[]
30
+ bunfig?: string
30
31
  envFile?: boolean
31
32
  stdin?: string
32
33
  }
package/dist/isolated.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // @bun
2
- import{mkdtempSync as R,rmSync as y,writeFileSync as u}from"fs";import{tmpdir as I}from"os";import{join as d}from"path";import c from"process";var v=2000;function b(t,e={}){let n=e.mode&&e.mode!=="auto"?e.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(t)?"test":"run";return[c.execPath,...e.config?[`--config=${e.config}`]:[],...e.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],t,...e.args??[]]}function h(t){let e=t.inheritEnv===!1?{}:{...c.env};for(let[n,r]of Object.entries(t.env??{}))if(r===void 0)delete e[n];else e[n]=r;return Object.fromEntries(Object.entries(e).filter((n)=>n[1]!==void 0))}async function k(t,e={}){if(!t||typeof t!=="string")throw TypeError("runIsolated needs a file to run");let n=e.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");let r=null,s;if(e.preload){r=R(d(I(),"stacks-isolated-"));let i=d(r,"noop-preload.ts");u(i,`export {}
3
- `);let o=JSON.stringify(e.preload.length?e.preload:[i]);s=d(r,"bunfig.toml"),u(s,`preload = ${o}
2
+ import{existsSync as p,mkdtempSync as h,rmSync as y,writeFileSync as c}from"fs";import{tmpdir as I}from"os";import{join as d,resolve as R}from"path";import l from"process";var v=2000;function E(t,e={}){let n=e.mode&&e.mode!=="auto"?e.mode:/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(t)?"test":"run";return[l.execPath,...e.config?[`--config=${e.config}`]:[],...e.envFile===!1?["--no-env-file"]:[],...n==="test"?["test"]:[],t,...e.args??[]]}function O(t){let e=t.inheritEnv===!1?{}:{...l.env};for(let[n,i]of Object.entries(t.env??{}))if(i===void 0)delete e[n];else e[n]=i;return Object.fromEntries(Object.entries(e).filter((n)=>n[1]!==void 0))}async function M(t,e={}){if(!t||typeof t!=="string")throw TypeError("runIsolated needs a file to run");let n=e.timeoutMs??60000;if(!Number.isFinite(n)||n<=0)throw RangeError("timeoutMs must be a positive number of milliseconds");if(e.bunfig!=null&&e.preload)throw TypeError("runIsolated takes bunfig or preload, not both: list the preloads in that bunfig");let i=null,o;if(e.bunfig!=null){if(!e.bunfig)throw TypeError("bunfig must be a path to a bunfig.toml");if(o=R(e.cwd??l.cwd(),e.bunfig),!p(o))throw Error(`runIsolated: no bunfig at ${o}`)}if(e.preload){i=h(d(I(),"stacks-isolated-"));let r=d(i,"noop-preload.ts");c(r,`export {}
3
+ `);let s=JSON.stringify(e.preload.length?e.preload:[r]);o=d(i,"bunfig.toml"),c(o,`preload = ${s}
4
4
 
5
5
  [test]
6
- preload = ${o}
7
- `)}let l=b(t,{...e,config:s}),m=Date.now(),a=!1;try{let i=Bun.spawn(l,{cwd:e.cwd,env:h(e),stdin:e.stdin!=null?new Blob([e.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),o,g=setTimeout(()=>{a=!0,i.kill("SIGTERM"),o=setTimeout(()=>i.kill("SIGKILL"),v)},n);try{let[f,p,w]=await Promise.all([i.exited,new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:i.signalCode?null:f,signal:i.signalCode==null?null:String(i.signalCode),stdout:p,stderr:w,timedOut:a,durationMs:Date.now()-m,command:l}}finally{if(clearTimeout(g),clearTimeout(o),i.exitCode===null&&!i.signalCode)i.kill("SIGKILL")}}finally{if(r)y(r,{recursive:!0,force:!0})}}function M(t,e){let n=`
6
+ preload = ${s}
7
+ `)}let a=E(t,{...e,config:o}),f=Date.now(),u=!1;try{let r=Bun.spawn(a,{cwd:e.cwd,env:O(e),stdin:e.stdin!=null?new Blob([e.stdin]):"ignore",stdout:"pipe",stderr:"pipe"}),s,g=setTimeout(()=>{u=!0,r.kill("SIGTERM"),s=setTimeout(()=>r.kill("SIGKILL"),v)},n);try{let[m,w,b]=await Promise.all([r.exited,new Response(r.stdout).text(),new Response(r.stderr).text()]);return{code:r.signalCode?null:m,signal:r.signalCode==null?null:String(r.signalCode),stdout:w,stderr:b,timedOut:u,durationMs:Date.now()-f,command:a}}finally{if(clearTimeout(g),clearTimeout(s),r.exitCode===null&&!r.signalCode)r.kill("SIGKILL")}}finally{if(i)y(i,{recursive:!0,force:!0})}}function j(t,e){let n=`
8
8
  --- stdout ---
9
9
  ${t.stdout}
10
10
  --- stderr ---
11
- ${t.stderr}`;if(t.timedOut)throw Error(`${t.command.slice(1).join(" ")} timed out after ${t.durationMs}ms${n}`);if(t.code!==0)throw Error(`${t.command.slice(1).join(" ")} exited ${t.code??t.signal}${n}`);if(e!=null&&!(typeof e==="string"?t.stdout.includes(e):e.test(t.stdout)))throw Error(`${t.command.slice(1).join(" ")} did not print ${String(e)}${n}`);return t}export{M as assertIsolatedOk,b as isolatedCommand,k as runIsolated};
11
+ ${t.stderr}`;if(t.timedOut)throw Error(`${t.command.slice(1).join(" ")} timed out after ${t.durationMs}ms${n}`);if(t.code!==0)throw Error(`${t.command.slice(1).join(" ")} exited ${t.code??t.signal}${n}`);if(e!=null&&!(typeof e==="string"?t.stdout.includes(e):e.test(t.stdout)))throw Error(`${t.command.slice(1).join(" ")} did not print ${String(e)}${n}`);return t}export{j as assertIsolatedOk,E as isolatedCommand,M as runIsolated};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/testing",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.230",
5
+ "version": "0.75.236",
6
6
  "description": "The Stacks way of testing.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -79,16 +79,16 @@
79
79
  "prepublishOnly": "bun run build"
80
80
  },
81
81
  "dependencies": {
82
- "@stacksjs/auth": "0.75.230",
83
- "@stacksjs/cache": "0.75.230",
84
- "@stacksjs/config": "0.75.230",
85
- "@stacksjs/database": "0.75.230",
86
- "@stacksjs/email": "0.75.230",
87
- "@stacksjs/events": "0.75.230",
88
- "@stacksjs/path": "0.75.230",
89
- "@stacksjs/router": "0.75.230",
90
- "@stacksjs/storage": "0.75.230",
91
- "@stacksjs/strings": "0.75.230",
82
+ "@stacksjs/auth": "0.75.236",
83
+ "@stacksjs/cache": "0.75.236",
84
+ "@stacksjs/config": "0.75.236",
85
+ "@stacksjs/database": "0.75.236",
86
+ "@stacksjs/email": "0.75.236",
87
+ "@stacksjs/events": "0.75.236",
88
+ "@stacksjs/path": "0.75.236",
89
+ "@stacksjs/router": "0.75.236",
90
+ "@stacksjs/storage": "0.75.236",
91
+ "@stacksjs/strings": "0.75.236",
92
92
  "@stacksjs/ts-cloud": "^0.16.58",
93
93
  "very-happy-dom": "^0.3.5"
94
94
  },