@stratal/testing 0.0.21 → 0.0.23

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 (36) hide show
  1. package/README.md +0 -17
  2. package/dist/database/index.d.mts +2 -0
  3. package/dist/database/index.mjs +2 -0
  4. package/dist/database-B02eYKhE.mjs +334 -0
  5. package/dist/database-B02eYKhE.mjs.map +1 -0
  6. package/dist/decorate-B7nr7eBl.mjs +9 -0
  7. package/dist/feature-flags/index.d.mts +2 -0
  8. package/dist/feature-flags/index.mjs +2 -0
  9. package/dist/feature-flags-BiLhfSGh.mjs +86 -0
  10. package/dist/feature-flags-BiLhfSGh.mjs.map +1 -0
  11. package/dist/index-BIr5nLof.d.mts +122 -0
  12. package/dist/index-BIr5nLof.d.mts.map +1 -0
  13. package/dist/{index-D-Q2cR2v.d.mts → index-CrHzUDKX.d.mts} +1 -1
  14. package/dist/index-CrHzUDKX.d.mts.map +1 -0
  15. package/dist/index-qgWNJRdC.d.mts +65 -0
  16. package/dist/index-qgWNJRdC.d.mts.map +1 -0
  17. package/dist/index.d.mts +29 -2
  18. package/dist/index.d.mts.map +1 -1
  19. package/dist/index.mjs +98 -68
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/mocks/zenstack-language.d.mts.map +1 -1
  22. package/dist/mocks/zenstack-language.mjs.map +1 -1
  23. package/dist/storage/index.d.mts +2 -2
  24. package/dist/storage/index.mjs +1 -1
  25. package/dist/{storage-CIXR3QUE.mjs → storage-DhoxWqyF.mjs} +5 -18
  26. package/dist/{storage-CIXR3QUE.mjs.map → storage-DhoxWqyF.mjs.map} +1 -1
  27. package/dist/vitest-plugin/index.d.mts +71 -5
  28. package/dist/vitest-plugin/index.d.mts.map +1 -1
  29. package/dist/vitest-plugin/index.mjs +82 -11
  30. package/dist/vitest-plugin/index.mjs.map +1 -1
  31. package/package.json +26 -18
  32. package/dist/index-D-Q2cR2v.d.mts.map +0 -1
  33. package/dist/mocks/nodemailer.d.mts +0 -12
  34. package/dist/mocks/nodemailer.d.mts.map +0 -1
  35. package/dist/mocks/nodemailer.mjs +0 -7
  36. package/dist/mocks/nodemailer.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"storage-CIXR3QUE.mjs","names":[],"sources":["../src/storage/fake-storage.service.ts"],"sourcesContent":["import { Transient, inject } from 'stratal/di'\nimport {\n FileNotFoundError,\n STORAGE_TOKENS,\n type StorageManagerService,\n StorageService,\n type StreamingBlobPayloadInputTypes,\n type DownloadResult,\n type PresignedUrlResult,\n type StorageConfig,\n type UploadOptions,\n type UploadResult,\n} from 'stratal/storage'\nimport { expect } from 'vitest'\n\n/**\n * Stored file representation in memory\n */\nexport interface StoredFile {\n content: Uint8Array\n mimeType: string\n size: number\n metadata?: Record<string, string>\n uploadedAt: Date\n}\n\n/**\n * FakeStorageService\n *\n * In-memory storage implementation for testing.\n * Registered by default in TestingModuleBuilder.\n *\n * Similar to Laravel's Storage::fake() - stores files in memory\n * and provides assertion helpers for testing.\n *\n * @example\n * ```typescript\n * // Access via TestingModule\n * module.storage.assertExists('path/to/file.pdf')\n * module.storage.assertMissing('deleted/file.pdf')\n * module.storage.clear() // Reset between tests\n * ```\n */\n@Transient(STORAGE_TOKENS.StorageService)\nexport class FakeStorageService extends StorageService {\n private files = new Map<string, StoredFile>()\n\n constructor(\n @inject(STORAGE_TOKENS.StorageManager)\n protected readonly storageManager: StorageManagerService,\n @inject(STORAGE_TOKENS.Options)\n protected readonly options: StorageConfig\n ) {\n super(storageManager, options)\n }\n\n /**\n * Upload content to fake storage\n */\n async upload(\n body: StreamingBlobPayloadInputTypes,\n relativePath: string,\n options: UploadOptions,\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const diskName = this.resolveDisk(disk)\n\n this.files.set(relativePath, {\n content,\n mimeType: options.mimeType ?? 'application/octet-stream',\n size: options.size,\n metadata: options.metadata,\n uploadedAt: new Date(),\n })\n\n return {\n path: relativePath,\n disk: diskName,\n fullPath: relativePath,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n /**\n * Download a file from fake storage\n */\n download(path: string): Promise<DownloadResult> {\n const file = this.files.get(path)\n\n if (!file) {\n return Promise.reject(new FileNotFoundError(path))\n }\n\n return Promise.resolve({\n toStream: () => new ReadableStream({\n start(controller) {\n controller.enqueue(file.content)\n controller.close()\n },\n }),\n toString: () => Promise.resolve(new TextDecoder().decode(file.content)),\n toArrayBuffer: () => Promise.resolve(file.content),\n contentType: file.mimeType,\n size: file.size,\n metadata: file.metadata,\n })\n }\n\n /**\n * Delete a file from fake storage\n */\n delete(path: string): Promise<void> {\n this.files.delete(path)\n return Promise.resolve()\n }\n\n /**\n * Check if a file exists in fake storage\n */\n exists(path: string): Promise<boolean> {\n return Promise.resolve(this.files.has(path))\n }\n\n /**\n * Generate a fake presigned download URL\n */\n getPresignedDownloadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'GET', expiresIn))\n }\n\n /**\n * Generate a fake presigned upload URL\n */\n getPresignedUploadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'PUT', expiresIn))\n }\n\n /**\n * Generate a fake presigned delete URL\n */\n getPresignedDeleteUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'DELETE', expiresIn))\n }\n\n /**\n * Chunked upload (same as regular upload for fake)\n */\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number },\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const size = options.size ?? content.length\n\n return this.upload(body, path, { ...options, size }, disk)\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Test Assertion Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Assert that a file exists at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file does not exist\n */\n assertExists(path: string): void {\n expect(\n this.files.has(path),\n `Expected file to exist at: ${path}\\nStored files: ${this.getStoredPaths().join(', ') || '(none)'}`\n ).toBe(true)\n }\n\n /**\n * Assert that a file does NOT exist at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file exists\n */\n assertMissing(path: string): void {\n expect(\n this.files.has(path),\n `Expected file NOT to exist at: ${path}`\n ).toBe(false)\n }\n\n /**\n * Assert storage is empty\n *\n * @throws AssertionError if any files exist\n */\n assertEmpty(): void {\n expect(\n this.files.size,\n `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(', ')}`\n ).toBe(0)\n }\n\n /**\n * Assert storage has exactly N files\n *\n * @param count - Expected number of files\n * @throws AssertionError if count doesn't match\n */\n assertCount(count: number): void {\n expect(\n this.files.size,\n `Expected ${count} files in storage but found ${this.files.size}`\n ).toBe(count)\n }\n\n /**\n * Get all stored files (for inspection)\n */\n getStoredFiles(): Map<string, StoredFile> {\n return new Map(this.files)\n }\n\n /**\n * Get all stored file paths\n */\n getStoredPaths(): string[] {\n return Array.from(this.files.keys())\n }\n\n /**\n * Get a specific file by path\n */\n getFile(path: string): StoredFile | undefined {\n return this.files.get(path)\n }\n\n /**\n * Clear all stored files (call in beforeEach for test isolation)\n */\n clear(): void {\n this.files.clear()\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Private Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n private createPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn = 300\n ): PresignedUrlResult {\n const expiresAt = new Date(Date.now() + expiresIn * 1000)\n\n return {\n url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,\n expiresIn,\n expiresAt,\n method,\n }\n }\n\n private async bodyToUint8Array(body: StreamingBlobPayloadInputTypes | null | undefined): Promise<Uint8Array> {\n if (!body) {\n return new Uint8Array(0)\n }\n\n if (body instanceof Uint8Array) {\n return body\n }\n\n if (body instanceof ArrayBuffer) {\n return new Uint8Array(body)\n }\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body)\n }\n\n if (body instanceof Blob) {\n const buffer = await body.arrayBuffer()\n return new Uint8Array(buffer)\n }\n\n if (body instanceof ReadableStream) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n // FormData or URLSearchParams - convert via Response\n if (body instanceof FormData || body instanceof URLSearchParams) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n return new Uint8Array(0)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage-DhoxWqyF.mjs","names":[],"sources":["../src/storage/fake-storage.service.ts"],"sourcesContent":["import { Transient, inject } from 'stratal/di'\nimport {\n FileNotFoundError,\n STORAGE_TOKENS,\n type StorageManagerService,\n StorageService,\n type StreamingBlobPayloadInputTypes,\n type DownloadResult,\n type PresignedUrlResult,\n type StorageConfig,\n type UploadOptions,\n type UploadResult,\n} from 'stratal/storage'\nimport { expect } from 'vitest'\n\n/**\n * Stored file representation in memory\n */\nexport interface StoredFile {\n content: Uint8Array\n mimeType: string\n size: number\n metadata?: Record<string, string>\n uploadedAt: Date\n}\n\n/**\n * FakeStorageService\n *\n * In-memory storage implementation for testing.\n * Registered by default in TestingModuleBuilder.\n *\n * Similar to Laravel's Storage::fake() - stores files in memory\n * and provides assertion helpers for testing.\n *\n * @example\n * ```typescript\n * // Access via TestingModule\n * module.storage.assertExists('path/to/file.pdf')\n * module.storage.assertMissing('deleted/file.pdf')\n * module.storage.clear() // Reset between tests\n * ```\n */\n@Transient(STORAGE_TOKENS.StorageService)\nexport class FakeStorageService extends StorageService {\n private files = new Map<string, StoredFile>()\n\n constructor(\n @inject(STORAGE_TOKENS.StorageManager)\n protected readonly storageManager: StorageManagerService,\n @inject(STORAGE_TOKENS.Options)\n protected readonly options: StorageConfig\n ) {\n super(storageManager, options)\n }\n\n /**\n * Upload content to fake storage\n */\n async upload(\n body: StreamingBlobPayloadInputTypes,\n relativePath: string,\n options: UploadOptions,\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const diskName = this.resolveDisk(disk)\n\n this.files.set(relativePath, {\n content,\n mimeType: options.mimeType ?? 'application/octet-stream',\n size: options.size,\n metadata: options.metadata,\n uploadedAt: new Date(),\n })\n\n return {\n path: relativePath,\n disk: diskName,\n fullPath: relativePath,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n /**\n * Download a file from fake storage\n */\n download(path: string): Promise<DownloadResult> {\n const file = this.files.get(path)\n\n if (!file) {\n return Promise.reject(new FileNotFoundError(path))\n }\n\n return Promise.resolve({\n toStream: () => new ReadableStream({\n start(controller) {\n controller.enqueue(file.content)\n controller.close()\n },\n }),\n toString: () => Promise.resolve(new TextDecoder().decode(file.content)),\n toArrayBuffer: () => Promise.resolve(file.content),\n contentType: file.mimeType,\n size: file.size,\n metadata: file.metadata,\n })\n }\n\n /**\n * Delete a file from fake storage\n */\n delete(path: string): Promise<void> {\n this.files.delete(path)\n return Promise.resolve()\n }\n\n /**\n * Check if a file exists in fake storage\n */\n exists(path: string): Promise<boolean> {\n return Promise.resolve(this.files.has(path))\n }\n\n /**\n * Generate a fake presigned download URL\n */\n getPresignedDownloadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'GET', expiresIn))\n }\n\n /**\n * Generate a fake presigned upload URL\n */\n getPresignedUploadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'PUT', expiresIn))\n }\n\n /**\n * Generate a fake presigned delete URL\n */\n getPresignedDeleteUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'DELETE', expiresIn))\n }\n\n /**\n * Chunked upload (same as regular upload for fake)\n */\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number },\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const size = options.size ?? content.length\n\n return this.upload(body, path, { ...options, size }, disk)\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Test Assertion Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Assert that a file exists at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file does not exist\n */\n assertExists(path: string): void {\n expect(\n this.files.has(path),\n `Expected file to exist at: ${path}\\nStored files: ${this.getStoredPaths().join(', ') || '(none)'}`\n ).toBe(true)\n }\n\n /**\n * Assert that a file does NOT exist at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file exists\n */\n assertMissing(path: string): void {\n expect(\n this.files.has(path),\n `Expected file NOT to exist at: ${path}`\n ).toBe(false)\n }\n\n /**\n * Assert storage is empty\n *\n * @throws AssertionError if any files exist\n */\n assertEmpty(): void {\n expect(\n this.files.size,\n `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(', ')}`\n ).toBe(0)\n }\n\n /**\n * Assert storage has exactly N files\n *\n * @param count - Expected number of files\n * @throws AssertionError if count doesn't match\n */\n assertCount(count: number): void {\n expect(\n this.files.size,\n `Expected ${count} files in storage but found ${this.files.size}`\n ).toBe(count)\n }\n\n /**\n * Get all stored files (for inspection)\n */\n getStoredFiles(): Map<string, StoredFile> {\n return new Map(this.files)\n }\n\n /**\n * Get all stored file paths\n */\n getStoredPaths(): string[] {\n return Array.from(this.files.keys())\n }\n\n /**\n * Get a specific file by path\n */\n getFile(path: string): StoredFile | undefined {\n return this.files.get(path)\n }\n\n /**\n * Clear all stored files (call in beforeEach for test isolation)\n */\n clear(): void {\n this.files.clear()\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Private Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n private createPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn = 300\n ): PresignedUrlResult {\n const expiresAt = new Date(Date.now() + expiresIn * 1000)\n\n return {\n url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,\n expiresIn,\n expiresAt,\n method,\n }\n }\n\n private async bodyToUint8Array(body: StreamingBlobPayloadInputTypes | null | undefined): Promise<Uint8Array> {\n if (!body) {\n return new Uint8Array(0)\n }\n\n if (body instanceof Uint8Array) {\n return body\n }\n\n if (body instanceof ArrayBuffer) {\n return new Uint8Array(body)\n }\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body)\n }\n\n if (body instanceof Blob) {\n const buffer = await body.arrayBuffer()\n return new Uint8Array(buffer)\n }\n\n if (body instanceof ReadableStream) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n // FormData or URLSearchParams - convert via Response\n if (body instanceof FormData || body instanceof URLSearchParams) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n return new Uint8Array(0)\n }\n}\n"],"mappings":"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"}
@@ -1,8 +1,42 @@
1
+ import { r as DatabaseIsolation } from "../index-BIr5nLof.mjs";
2
+ import { StratalEnv } from "stratal";
1
3
  import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
2
4
  import { Plugin } from "vite";
3
5
 
4
6
  //#region src/vitest-plugin/stratal-test.d.ts
5
7
  type CloudflareTestOptions = Parameters<typeof cloudflareTest>[0];
8
+ /** String keys of `StratalEnv` whose value is a Hyperdrive binding. */
9
+ type HyperdriveKeys = Extract<{ [K in keyof StratalEnv]-?: StratalEnv[K] extends Hyperdrive ? K : never }[keyof StratalEnv], string>;
10
+ /**
11
+ * Names of declared Hyperdrive bindings, drawn from the consumer's augmented
12
+ * `StratalEnv` (which extends `Cloudflare.Env`). Falls back to `string` only
13
+ * when no Hyperdrive binding is declared (nothing to constrain to).
14
+ */
15
+ type HyperdriveBindingName = [HyperdriveKeys] extends [never] ? string : HyperdriveKeys;
16
+ /** Stratal-specific test database configuration for {@link stratalTest}. */
17
+ interface StratalTestDatabaseOptions {
18
+ /**
19
+ * Database isolation mode for parallel runs. Defaults to `'shared'`.
20
+ *
21
+ * - `'shared'` — all test files share one database (serial; today's behaviour).
22
+ * - `'database'` — each test file gets its own database cloned from a migrated
23
+ * template (dropped on teardown), and file parallelism is enabled.
24
+ *
25
+ * Pair with `createTestDatabaseGlobalSetup({ isolation })` from
26
+ * `@stratal/testing/database` in your Vitest `globalSetup`.
27
+ */
28
+ isolation?: DatabaseIsolation;
29
+ /**
30
+ * Name of the Hyperdrive binding to isolate per test file. Defaults to `'DB'`.
31
+ * Constrained to Hyperdrive binding names declared on `Cloudflare.Env` /
32
+ * `StratalEnv`.
33
+ */
34
+ binding?: HyperdriveBindingName;
35
+ }
36
+ type WorkersPoolOptions = Exclude<CloudflareTestOptions, (...args: never[]) => unknown>;
37
+ type StratalTestOptions = WorkersPoolOptions & {
38
+ database?: StratalTestDatabaseOptions;
39
+ };
6
40
  /**
7
41
  * Returns a Vite plugin that forces CJS resolution for `pg` sub-dependencies.
8
42
  *
@@ -33,18 +67,50 @@ type CloudflareTestOptions = Parameters<typeof cloudflareTest>[0];
33
67
  * ```
34
68
  */
35
69
  declare const fixPgCjs: () => Plugin;
70
+ /**
71
+ * Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths
72
+ * used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).
73
+ *
74
+ * cuid2@2.x is CJS and does `require("@noble/hashes/sha3")` without the `.js`
75
+ * extension. In a workspace that also installs `@noble/hashes@2.x` (ESM-only),
76
+ * the hoisted v2 package has no extensionless `./sha3` entry in its exports
77
+ * map, so Vite/workerd resolution fails. This plugin routes the extensionless
78
+ * subpaths through the consumer's nested `@noble/hashes@1.x` (a sibling of
79
+ * cuid2 under `@zenstackhq/orm/node_modules`), which ships proper CJS exports.
80
+ *
81
+ * If the consumer doesn't depend on `@zenstackhq/orm`, the plugin is a no-op.
82
+ * Otherwise it throws loudly at config-resolution time if the resolution chain
83
+ * is unexpectedly broken — surfacing dependency drift instead of letting the
84
+ * symptom resurface as an opaque test failure.
85
+ *
86
+ * Must be used at the **root** `defineConfig` level (same constraint as
87
+ * `fixPgCjs`).
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { fixNobleHashesCjs, fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'
92
+ *
93
+ * export default defineConfig({
94
+ * plugins: [fixPgCjs(), fixNobleHashesCjs()],
95
+ * // ...
96
+ * })
97
+ * ```
98
+ */
99
+ declare const fixNobleHashesCjs: () => Plugin;
36
100
  /**
37
101
  * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.
38
102
  *
39
- * Includes the cloudflare pool plugin and Stratal alias plugin.
40
- * Use inside a project-level `plugins` array.
103
+ * Includes the cloudflare pool plugin and Stratal alias plugin. Pass
104
+ * `database: { isolation: 'database' }` to give each test file its own
105
+ * database (cloned from a migrated template, dropped on teardown) and enable
106
+ * file parallelism. Use inside a project-level `plugins` array.
41
107
  *
42
108
  * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.
43
109
  *
44
- * @param options - Same options as `cloudflareTest()` from `@cloudflare/vitest-pool-workers`
110
+ * @param options - `cloudflareTest()` options plus Stratal `database` options
45
111
  * @returns An array of Vite plugins
46
112
  */
47
- declare function stratalTest(options?: CloudflareTestOptions): Plugin[];
113
+ declare function stratalTest(options?: StratalTestOptions): Plugin[];
48
114
  //#endregion
49
- export { fixPgCjs, stratalTest };
115
+ export { type StratalTestDatabaseOptions, fixNobleHashesCjs, fixPgCjs, stratalTest };
50
116
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"mappings":";;;;KAQK,qBAAA,GAAwB,UAAA,QAAkB,cAAA;;AAJD;;;;;AA0C9C;;;;;AA6CA;;;;;;;;;;;;;;;;;;cA7Ca,QAAA,QAAe,MAAA;;;;;;;;;;;;iBA6CZ,WAAA,CAAY,OAAA,GAAS,qBAAA,GAA6B,MAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"mappings":";;;;;;KAWK,qBAAA,GAAwB,UAAU,QAAQ,cAAA;;KAG1C,cAAA,GAAiB,OAAA,eACN,UAAA,KAAe,UAAA,CAAW,CAAA,UAAW,UAAA,GAAa,CAAA,iBAAkB,UAAA;;AAJvB;AAAA;;;KAaxD,qBAAA,IAAyB,cAAA,6BAA2C,cAAc;;UAGtE,0BAAA;EAZoC;;;;;;;;;;EAuBnD,SAAA,GAAY,iBAAA;EAvBuC;;;;AAAyC;EA6B5F,OAAA,GAAU,qBAAqB;AAAA;AAAA,KAG5B,kBAAA,GAAqB,OAAO,CAAC,qBAAA,MAA2B,IAAA;AAAA,KACxD,kBAAA,GAAqB,kBAAA;EAAuB,QAAA,GAAW,0BAA0B;AAAA;;;;;;;;;AAJrD;AAChC;;;;;;;;AAEgE;AAAA;;;;;;;;AACqB;AAsCtF;;cAAa,QAAA,QAAe,MAY1B;;AAAA;AA+BF;;;;AA0BC;AA4CD;;;;;;;;AAAqE;;;;;;;;;;;;;;cAtExD,iBAAA,QAAwB,MA0BpC;;;;;;;;;;;;;;iBA4Ce,WAAA,CAAY,OAAA,GAAS,kBAAA,GAA0B,MAAM"}
@@ -1,3 +1,4 @@
1
+ import { f as normalizeIsolation, r as ISOLATION_ENV_VAR, t as BINDING_ENV_VAR } from "../database-B02eYKhE.mjs";
1
2
  import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
@@ -51,36 +52,106 @@ const fixPgCjs = () => ({
51
52
  }
52
53
  }
53
54
  });
54
- const stratalPlugin = {
55
+ /**
56
+ * Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths
57
+ * used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).
58
+ *
59
+ * cuid2@2.x is CJS and does `require("@noble/hashes/sha3")` without the `.js`
60
+ * extension. In a workspace that also installs `@noble/hashes@2.x` (ESM-only),
61
+ * the hoisted v2 package has no extensionless `./sha3` entry in its exports
62
+ * map, so Vite/workerd resolution fails. This plugin routes the extensionless
63
+ * subpaths through the consumer's nested `@noble/hashes@1.x` (a sibling of
64
+ * cuid2 under `@zenstackhq/orm/node_modules`), which ships proper CJS exports.
65
+ *
66
+ * If the consumer doesn't depend on `@zenstackhq/orm`, the plugin is a no-op.
67
+ * Otherwise it throws loudly at config-resolution time if the resolution chain
68
+ * is unexpectedly broken — surfacing dependency drift instead of letting the
69
+ * symptom resurface as an opaque test failure.
70
+ *
71
+ * Must be used at the **root** `defineConfig` level (same constraint as
72
+ * `fixPgCjs`).
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { fixNobleHashesCjs, fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'
77
+ *
78
+ * export default defineConfig({
79
+ * plugins: [fixPgCjs(), fixNobleHashesCjs()],
80
+ * // ...
81
+ * })
82
+ * ```
83
+ */
84
+ const fixNobleHashesCjs = () => {
85
+ const ids = ["@noble/hashes/sha3", "@noble/hashes/crypto"];
86
+ let resolved = null;
87
+ return {
88
+ name: "stratal-noble-hashes-cjs",
89
+ enforce: "pre",
90
+ configResolved(config) {
91
+ const consumerRequire = createRequire(path.join(config.root, "noop.js"));
92
+ let zenstackPath;
93
+ try {
94
+ zenstackPath = consumerRequire.resolve("@zenstackhq/orm");
95
+ } catch {
96
+ return;
97
+ }
98
+ const cuid2Require = createRequire(createRequire(zenstackPath).resolve("@paralleldrive/cuid2"));
99
+ resolved = new Map(ids.map((id) => [id, cuid2Require.resolve(id)]));
100
+ },
101
+ resolveId(id) {
102
+ return resolved?.get(id);
103
+ }
104
+ };
105
+ };
106
+ const createStratalPlugin = (isolation) => ({
55
107
  name: "stratal-test",
56
108
  config() {
57
- return {
109
+ const config = {
58
110
  resolve: { alias: {
59
- tslib: "tsyringe/node_modules/tslib/tslib.es6.js",
111
+ tslib: "tslib/tslib.es6.mjs",
60
112
  "@zenstackhq/language/ast": "@stratal/testing/mocks/zenstack-language",
61
113
  "@zenstackhq/language/utils": "@stratal/testing/mocks/zenstack-language",
62
- "@zenstackhq/language": "@stratal/testing/mocks/zenstack-language",
63
- nodemailer: "@stratal/testing/mocks/nodemailer"
114
+ "@zenstackhq/language": "@stratal/testing/mocks/zenstack-language"
64
115
  } },
65
116
  ssr: { noExternal: ["@zenstackhq/better-auth"] }
66
117
  };
118
+ if (isolation === "database") config.test = {
119
+ fileParallelism: true,
120
+ isolate: true
121
+ };
122
+ return config;
67
123
  }
68
- };
124
+ });
69
125
  /**
70
126
  * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.
71
127
  *
72
- * Includes the cloudflare pool plugin and Stratal alias plugin.
73
- * Use inside a project-level `plugins` array.
128
+ * Includes the cloudflare pool plugin and Stratal alias plugin. Pass
129
+ * `database: { isolation: 'database' }` to give each test file its own
130
+ * database (cloned from a migrated template, dropped on teardown) and enable
131
+ * file parallelism. Use inside a project-level `plugins` array.
74
132
  *
75
133
  * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.
76
134
  *
77
- * @param options - Same options as `cloudflareTest()` from `@cloudflare/vitest-pool-workers`
135
+ * @param options - `cloudflareTest()` options plus Stratal `database` options
78
136
  * @returns An array of Vite plugins
79
137
  */
80
138
  function stratalTest(options = {}) {
81
- return [cloudflareTest(options), stratalPlugin];
139
+ const { database, ...cfOptions } = options;
140
+ const isolation = normalizeIsolation(database?.isolation);
141
+ const binding = database?.binding ?? "DB";
142
+ return [cloudflareTest({
143
+ ...cfOptions,
144
+ miniflare: {
145
+ ...cfOptions.miniflare,
146
+ bindings: {
147
+ ...cfOptions.miniflare?.bindings,
148
+ [ISOLATION_ENV_VAR]: isolation,
149
+ [BINDING_ENV_VAR]: binding
150
+ }
151
+ }
152
+ }), createStratalPlugin(isolation)];
82
153
  }
83
154
  //#endregion
84
- export { fixPgCjs, stratalTest };
155
+ export { fixNobleHashesCjs, fixPgCjs, stratalTest };
85
156
 
86
157
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport path from 'node:path'\n\nimport { cloudflareTest } from '@cloudflare/vitest-pool-workers'\nimport type { Plugin, UserConfig } from 'vite'\n\nconst require = createRequire(import.meta.url)\n\ntype CloudflareTestOptions = Parameters<typeof cloudflareTest>[0]\n\nconst pgCjsResolvers = new Map<string, () => string>([\n ['pg-protocol', () => require.resolve('pg-protocol')],\n ['pg-connection-string', () => require.resolve('pg-connection-string')],\n ['pg-pool', () => require.resolve('pg-pool')],\n ['pg-cloudflare', () => path.join(path.dirname(require.resolve('pg-cloudflare')), 'index.js')],\n])\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `pg` sub-dependencies.\n *\n * `pg` is CJS but its dependencies (`pg-protocol`, `pg-connection-string`, `pg-pool`) ship\n * dual CJS/ESM exports. In workerd, the module fallback resolver prefers the ESM condition,\n * causing `SyntaxError: Cannot use import statement outside a module` when CJS `pg` does\n * `require()`. Additionally, `pg-cloudflare` uses a `workerd` export condition that the\n * root Vite instance doesn't resolve.\n *\n * Must be used at the **root** `defineConfig` level so that the\n * `@cloudflare/vitest-pool-workers` module fallback resolver (which uses the root Vite\n * instance) resolves pg sub-deps correctly.\n *\n * @example\n * ```ts\n * import { fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n * import { defineConfig } from 'vitest/config'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs()],\n * test: {\n * projects: [{\n * plugins: [stratalTest({ wrangler: { configPath: './wrangler.jsonc' } })],\n * test: { name: 'e2e', include: ['test/e2e/**\\/*.spec.ts'] },\n * }],\n * },\n * })\n * ```\n */\nexport const fixPgCjs = (): Plugin => ({\n name: 'stratal-pg-cjs',\n enforce: 'pre',\n resolveId(id) {\n const resolver = pgCjsResolvers.get(id)\n if (!resolver) return\n try {\n return resolver()\n } catch {\n return\n }\n },\n})\n\nconst stratalPlugin: Plugin = {\n name: 'stratal-test',\n config() {\n return {\n resolve: {\n alias: {\n tslib: 'tsyringe/node_modules/tslib/tslib.es6.js',\n '@zenstackhq/language/ast': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language/utils': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language': '@stratal/testing/mocks/zenstack-language',\n nodemailer: '@stratal/testing/mocks/nodemailer',\n },\n },\n ssr: {\n noExternal: ['@zenstackhq/better-auth'],\n },\n } satisfies UserConfig\n },\n}\n\n/**\n * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.\n *\n * Includes the cloudflare pool plugin and Stratal alias plugin.\n * Use inside a project-level `plugins` array.\n *\n * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.\n *\n * @param options - Same options as `cloudflareTest()` from `@cloudflare/vitest-pool-workers`\n * @returns An array of Vite plugins\n */\nexport function stratalTest(options: CloudflareTestOptions = {}): Plugin[] {\n return [cloudflareTest(options) as unknown as Plugin, stratalPlugin]\n}\n"],"mappings":";;;;AAMA,MAAM,UAAU,cAAc,OAAO,KAAK,IAAI;AAI9C,MAAM,iBAAiB,IAAI,IAA0B;CACnD,CAAC,qBAAqB,QAAQ,QAAQ,cAAc,CAAC;CACrD,CAAC,8BAA8B,QAAQ,QAAQ,uBAAuB,CAAC;CACvE,CAAC,iBAAiB,QAAQ,QAAQ,UAAU,CAAC;CAC7C,CAAC,uBAAuB,KAAK,KAAK,KAAK,QAAQ,QAAQ,QAAQ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAC/F,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BF,MAAa,kBAA0B;CACrC,MAAM;CACN,SAAS;CACT,UAAU,IAAI;EACZ,MAAM,WAAW,eAAe,IAAI,GAAG;EACvC,IAAI,CAAC,UAAU;EACf,IAAI;GACF,OAAO,UAAU;UACX;GACN;;;CAGL;AAED,MAAM,gBAAwB;CAC5B,MAAM;CACN,SAAS;EACP,OAAO;GACL,SAAS,EACP,OAAO;IACL,OAAO;IACP,4BAA4B;IAC5B,8BAA8B;IAC9B,wBAAwB;IACxB,YAAY;IACb,EACF;GACD,KAAK,EACH,YAAY,CAAC,0BAA0B,EACxC;GACF;;CAEJ;;;;;;;;;;;;AAaD,SAAgB,YAAY,UAAiC,EAAE,EAAY;CACzE,OAAO,CAAC,eAAe,QAAQ,EAAuB,cAAc"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/vitest-plugin/stratal-test.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport path from 'node:path'\n\nimport { cloudflareTest } from '@cloudflare/vitest-pool-workers'\nimport type { StratalEnv } from 'stratal'\nimport type { Plugin, UserConfig } from 'vite'\nimport type { TestUserConfig } from 'vitest/config'\nimport { BINDING_ENV_VAR, DEFAULT_DB_BINDING, ISOLATION_ENV_VAR, normalizeIsolation, type DatabaseIsolation } from '../database'\n\nconst require = createRequire(import.meta.url)\n\ntype CloudflareTestOptions = Parameters<typeof cloudflareTest>[0]\n\n/** String keys of `StratalEnv` whose value is a Hyperdrive binding. */\ntype HyperdriveKeys = Extract<\n { [K in keyof StratalEnv]-?: StratalEnv[K] extends Hyperdrive ? K : never }[keyof StratalEnv],\n string\n>\n\n/**\n * Names of declared Hyperdrive bindings, drawn from the consumer's augmented\n * `StratalEnv` (which extends `Cloudflare.Env`). Falls back to `string` only\n * when no Hyperdrive binding is declared (nothing to constrain to).\n */\ntype HyperdriveBindingName = [HyperdriveKeys] extends [never] ? string : HyperdriveKeys\n\n/** Stratal-specific test database configuration for {@link stratalTest}. */\nexport interface StratalTestDatabaseOptions {\n /**\n * Database isolation mode for parallel runs. Defaults to `'shared'`.\n *\n * - `'shared'` — all test files share one database (serial; today's behaviour).\n * - `'database'` — each test file gets its own database cloned from a migrated\n * template (dropped on teardown), and file parallelism is enabled.\n *\n * Pair with `createTestDatabaseGlobalSetup({ isolation })` from\n * `@stratal/testing/database` in your Vitest `globalSetup`.\n */\n isolation?: DatabaseIsolation\n /**\n * Name of the Hyperdrive binding to isolate per test file. Defaults to `'DB'`.\n * Constrained to Hyperdrive binding names declared on `Cloudflare.Env` /\n * `StratalEnv`.\n */\n binding?: HyperdriveBindingName\n}\n\ntype WorkersPoolOptions = Exclude<CloudflareTestOptions, (...args: never[]) => unknown>\ntype StratalTestOptions = WorkersPoolOptions & { database?: StratalTestDatabaseOptions }\n\nconst pgCjsResolvers = new Map<string, () => string>([\n ['pg-protocol', () => require.resolve('pg-protocol')],\n ['pg-connection-string', () => require.resolve('pg-connection-string')],\n ['pg-pool', () => require.resolve('pg-pool')],\n ['pg-cloudflare', () => path.join(path.dirname(require.resolve('pg-cloudflare')), 'index.js')],\n])\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `pg` sub-dependencies.\n *\n * `pg` is CJS but its dependencies (`pg-protocol`, `pg-connection-string`, `pg-pool`) ship\n * dual CJS/ESM exports. In workerd, the module fallback resolver prefers the ESM condition,\n * causing `SyntaxError: Cannot use import statement outside a module` when CJS `pg` does\n * `require()`. Additionally, `pg-cloudflare` uses a `workerd` export condition that the\n * root Vite instance doesn't resolve.\n *\n * Must be used at the **root** `defineConfig` level so that the\n * `@cloudflare/vitest-pool-workers` module fallback resolver (which uses the root Vite\n * instance) resolves pg sub-deps correctly.\n *\n * @example\n * ```ts\n * import { fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n * import { defineConfig } from 'vitest/config'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs()],\n * test: {\n * projects: [{\n * plugins: [stratalTest({ wrangler: { configPath: './wrangler.jsonc' } })],\n * test: { name: 'e2e', include: ['test/e2e/**\\/*.spec.ts'] },\n * }],\n * },\n * })\n * ```\n */\nexport const fixPgCjs = (): Plugin => ({\n name: 'stratal-pg-cjs',\n enforce: 'pre',\n resolveId(id) {\n const resolver = pgCjsResolvers.get(id)\n if (!resolver) return\n try {\n return resolver()\n } catch {\n return\n }\n },\n})\n\n/**\n * Returns a Vite plugin that forces CJS resolution for `@noble/hashes` subpaths\n * used by `@paralleldrive/cuid2@2.x` (the version `@zenstackhq/orm` depends on).\n *\n * cuid2@2.x is CJS and does `require(\"@noble/hashes/sha3\")` without the `.js`\n * extension. In a workspace that also installs `@noble/hashes@2.x` (ESM-only),\n * the hoisted v2 package has no extensionless `./sha3` entry in its exports\n * map, so Vite/workerd resolution fails. This plugin routes the extensionless\n * subpaths through the consumer's nested `@noble/hashes@1.x` (a sibling of\n * cuid2 under `@zenstackhq/orm/node_modules`), which ships proper CJS exports.\n *\n * If the consumer doesn't depend on `@zenstackhq/orm`, the plugin is a no-op.\n * Otherwise it throws loudly at config-resolution time if the resolution chain\n * is unexpectedly broken — surfacing dependency drift instead of letting the\n * symptom resurface as an opaque test failure.\n *\n * Must be used at the **root** `defineConfig` level (same constraint as\n * `fixPgCjs`).\n *\n * @example\n * ```ts\n * import { fixNobleHashesCjs, fixPgCjs, stratalTest } from '@stratal/testing/vitest-plugin'\n *\n * export default defineConfig({\n * plugins: [fixPgCjs(), fixNobleHashesCjs()],\n * // ...\n * })\n * ```\n */\nexport const fixNobleHashesCjs = (): Plugin => {\n const ids = ['@noble/hashes/sha3', '@noble/hashes/crypto']\n let resolved: Map<string, string> | null = null\n\n return {\n name: 'stratal-noble-hashes-cjs',\n enforce: 'pre',\n configResolved(config) {\n const consumerRequire = createRequire(path.join(config.root, 'noop.js'))\n let zenstackPath: string\n try {\n zenstackPath = consumerRequire.resolve('@zenstackhq/orm')\n } catch {\n // Consumer doesn't use ZenStack — nothing to fix.\n return\n }\n const cuid2Path = createRequire(zenstackPath).resolve('@paralleldrive/cuid2')\n const cuid2Require = createRequire(cuid2Path)\n resolved = new Map<string, string>(\n ids.map((id) => [id, cuid2Require.resolve(id)]),\n )\n },\n resolveId(id) {\n return resolved?.get(id)\n },\n }\n}\n\nconst createStratalPlugin = (isolation: DatabaseIsolation): Plugin => ({\n name: 'stratal-test',\n config() {\n const config: UserConfig & { test?: TestUserConfig } = {\n resolve: {\n alias: {\n tslib: 'tslib/tslib.es6.mjs',\n '@zenstackhq/language/ast': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language/utils': '@stratal/testing/mocks/zenstack-language',\n '@zenstackhq/language': '@stratal/testing/mocks/zenstack-language',\n },\n },\n ssr: {\n noExternal: ['@zenstackhq/better-auth'],\n },\n }\n\n // In 'database' mode each test file owns its database, so enable file\n // parallelism + isolation. In 'shared' mode leave these untouched so the\n // project's own defaults stand (forcing them would alter maxWorkers and\n // can collide with sibling projects' sequence.groupOrder).\n if (isolation === 'database') {\n config.test = { fileParallelism: true, isolate: true }\n }\n\n return config\n },\n})\n\n/**\n * Returns Vite plugins for Stratal tests running in the Cloudflare Workers (workerd) environment.\n *\n * Includes the cloudflare pool plugin and Stratal alias plugin. Pass\n * `database: { isolation: 'database' }` to give each test file its own\n * database (cloned from a migrated template, dropped on teardown) and enable\n * file parallelism. Use inside a project-level `plugins` array.\n *\n * **Note:** `fixPgCjs()` must be registered separately at the root `defineConfig` level.\n *\n * @param options - `cloudflareTest()` options plus Stratal `database` options\n * @returns An array of Vite plugins\n */\nexport function stratalTest(options: StratalTestOptions = {}): Plugin[] {\n const { database, ...cfOptions } = options\n const isolation = normalizeIsolation(database?.isolation)\n const binding = database?.binding ?? DEFAULT_DB_BINDING\n\n // Expose the mode + binding name to the worker as env vars so\n // TestingModule.compile() can decide whether and what to provision.\n const merged = {\n ...cfOptions,\n miniflare: {\n ...cfOptions.miniflare,\n bindings: {\n ...(cfOptions.miniflare?.bindings as Record<string, unknown> | undefined),\n [ISOLATION_ENV_VAR]: isolation,\n [BINDING_ENV_VAR]: binding,\n },\n },\n }\n\n return [cloudflareTest(merged) as unknown as Plugin, createStratalPlugin(isolation)]\n}\n"],"mappings":";;;;;AASA,MAAM,UAAU,cAAc,OAAO,KAAK,GAAG;AAyC7C,MAAM,iBAAiB,IAAI,IAA0B;CACnD,CAAC,qBAAqB,QAAQ,QAAQ,aAAa,CAAC;CACpD,CAAC,8BAA8B,QAAQ,QAAQ,sBAAsB,CAAC;CACtE,CAAC,iBAAiB,QAAQ,QAAQ,SAAS,CAAC;CAC5C,CAAC,uBAAuB,KAAK,KAAK,KAAK,QAAQ,QAAQ,QAAQ,eAAe,CAAC,GAAG,UAAU,CAAC;AAC/F,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BD,MAAa,kBAA0B;CACrC,MAAM;CACN,SAAS;CACT,UAAU,IAAI;EACZ,MAAM,WAAW,eAAe,IAAI,EAAE;EACtC,IAAI,CAAC,UAAU;EACf,IAAI;GACF,OAAO,SAAS;EAClB,QAAQ;GACN;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,0BAAkC;CAC7C,MAAM,MAAM,CAAC,sBAAsB,sBAAsB;CACzD,IAAI,WAAuC;CAE3C,OAAO;EACL,MAAM;EACN,SAAS;EACT,eAAe,QAAQ;GACrB,MAAM,kBAAkB,cAAc,KAAK,KAAK,OAAO,MAAM,SAAS,CAAC;GACvE,IAAI;GACJ,IAAI;IACF,eAAe,gBAAgB,QAAQ,iBAAiB;GAC1D,QAAQ;IAEN;GACF;GAEA,MAAM,eAAe,cADH,cAAc,YAAY,EAAE,QAAQ,sBACX,CAAC;GAC5C,WAAW,IAAI,IACb,IAAI,KAAK,OAAO,CAAC,IAAI,aAAa,QAAQ,EAAE,CAAC,CAAC,CAChD;EACF;EACA,UAAU,IAAI;GACZ,OAAO,UAAU,IAAI,EAAE;EACzB;CACF;AACF;AAEA,MAAM,uBAAuB,eAA0C;CACrE,MAAM;CACN,SAAS;EACP,MAAM,SAAiD;GACrD,SAAS,EACP,OAAO;IACL,OAAO;IACP,4BAA4B;IAC5B,8BAA8B;IAC9B,wBAAwB;GAC1B,EACF;GACA,KAAK,EACH,YAAY,CAAC,yBAAyB,EACxC;EACF;EAMA,IAAI,cAAc,YAChB,OAAO,OAAO;GAAE,iBAAiB;GAAM,SAAS;EAAK;EAGvD,OAAO;CACT;AACF;;;;;;;;;;;;;;AAeA,SAAgB,YAAY,UAA8B,CAAC,GAAa;CACtE,MAAM,EAAE,UAAU,GAAG,cAAc;CACnC,MAAM,YAAY,mBAAmB,UAAU,SAAS;CACxD,MAAM,UAAU,UAAU,WAAA;CAgB1B,OAAO,CAAC,eAAe;EAXrB,GAAG;EACH,WAAW;GACT,GAAG,UAAU;GACb,UAAU;IACR,GAAI,UAAU,WAAW;KACxB,oBAAoB;KACpB,kBAAkB;GACrB;EACF;CAG0B,CAAC,GAAwB,oBAAoB,SAAS,CAAC;AACrF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratal/testing",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Testing utilities and mocks for Stratal framework applications",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,14 +37,18 @@
37
37
  "types": "./dist/index.d.mts",
38
38
  "import": "./dist/index.mjs"
39
39
  },
40
+ "./database": {
41
+ "types": "./dist/database/index.d.mts",
42
+ "import": "./dist/database/index.mjs"
43
+ },
44
+ "./feature-flags": {
45
+ "types": "./dist/feature-flags/index.d.mts",
46
+ "import": "./dist/feature-flags/index.mjs"
47
+ },
40
48
  "./mocks": {
41
49
  "types": "./dist/mocks/index.d.mts",
42
50
  "import": "./dist/mocks/index.mjs"
43
51
  },
44
- "./mocks/nodemailer": {
45
- "types": "./dist/mocks/nodemailer.d.mts",
46
- "import": "./dist/mocks/nodemailer.mjs"
47
- },
48
52
  "./mocks/zenstack-language": {
49
53
  "types": "./dist/mocks/zenstack-language.d.mts",
50
54
  "import": "./dist/mocks/zenstack-language.mjs"
@@ -66,15 +70,15 @@
66
70
  "lint:fix": "npx oxlint --fix ."
67
71
  },
68
72
  "dependencies": {
69
- "@cloudflare/vitest-pool-workers": "^0.16.3",
73
+ "@cloudflare/vitest-pool-workers": "^0.16.12",
70
74
  "@golevelup/ts-vitest": "^4.0.0",
71
- "msw": "^2.14.5"
75
+ "msw": "^2.14.6"
72
76
  },
73
77
  "peerDependencies": {
74
- "@stratal/framework": ">=0.0.21",
78
+ "@stratal/framework": ">=0.0.23",
75
79
  "better-auth": ">=1.4",
76
- "reflect-metadata": ">=0.2",
77
- "stratal": ">=0.0.21",
80
+ "pg": "^8.0.0",
81
+ "stratal": ">=0.0.23",
78
82
  "vitest": "^4.1.0"
79
83
  },
80
84
  "peerDependenciesMeta": {
@@ -83,19 +87,23 @@
83
87
  },
84
88
  "better-auth": {
85
89
  "optional": true
90
+ },
91
+ "pg": {
92
+ "optional": true
86
93
  }
87
94
  },
88
95
  "devDependencies": {
89
- "@cloudflare/workers-types": "4.20260510.1",
96
+ "@cloudflare/workers-types": "4.20260603.1",
90
97
  "@stratal/framework": "workspace:*",
91
- "@types/node": "^25.6.2",
92
- "@vitest/runner": "~4.1.5",
93
- "@vitest/snapshot": "~4.1.5",
94
- "better-auth": "^1.6.10",
95
- "reflect-metadata": "^0.2.2",
98
+ "@types/node": "^25.9.1",
99
+ "@types/pg": "^8.20.0",
100
+ "@vitest/runner": "~4.1.8",
101
+ "@vitest/snapshot": "~4.1.8",
102
+ "better-auth": "^1.6.14",
103
+ "pg": "^8.21.0",
96
104
  "stratal": "workspace:*",
97
- "tsdown": "^0.22.0",
105
+ "tsdown": "^0.22.1",
98
106
  "typescript": "^6.0.3",
99
- "vitest": "~4.1.5"
107
+ "vitest": "~4.1.8"
100
108
  }
101
109
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-D-Q2cR2v.d.mts","names":[],"sources":["../src/storage/fake-storage.service.ts"],"mappings":";;;;;AAkBA;UAAiB,UAAA;EACf,OAAA,EAAS,UAAA;EACT,QAAA;EACA,IAAA;EACA,QAAA,GAAW,MAAA;EACX,UAAA,EAAY,IAAA;AAAA;;;;;;;;;;;AAoBd;;;;;;;cACa,kBAAA,SAA2B,cAAA;EAAA,mBAKjB,cAAA,EAAgB,qBAAA;EAAA,mBAEhB,OAAA,EAAS,aAAA;EAAA,QANtB,KAAA;cAIa,cAAA,EAAgB,qBAAA,EAEhB,OAAA,EAAS,aAAA;EAsCN;;;EA9BlB,MAAA,CACJ,IAAA,EAAM,8BAAA,EACN,YAAA,UACA,OAAA,EAAS,aAAA,EACT,IAAA,YACC,OAAA,CAAQ,YAAA;EAoER;;;EA3CH,QAAA,CAAS,IAAA,WAAe,OAAA,CAAQ,cAAA;EA+D7B;;;EAtCH,MAAA,CAAO,IAAA,WAAe,OAAA;EAkDX;;;EA1CX,MAAA,CAAO,IAAA,WAAe,OAAA;EAyHC;;;EAlHvB,uBAAA,CACE,IAAA,UACA,SAAA,YACC,OAAA,CAAQ,kBAAA;EAxF2B;;;EA+FtC,qBAAA,CACE,IAAA,UACA,SAAA,YACC,OAAA,CAAQ,kBAAA;EA3FmB;;;EAkG9B,qBAAA,CACE,IAAA,UACA,SAAA,YACC,OAAA,CAAQ,kBAAA;EAvGU;;;EA8Gf,aAAA,CACJ,IAAA,EAAM,8BAAA,EACN,IAAA,UACA,OAAA,EAAS,IAAA,CAAK,aAAA;IAA2B,IAAA;EAAA,GACzC,IAAA,YACC,OAAA,CAAQ,YAAA;EAvGT;;;;;;EAwHF,YAAA,CAAa,IAAA;EA5FJ;;;;;;EAyGT,aAAA,CAAc,IAAA;EAxEP;;;;;EAoFP,WAAA,CAAA;EA1EW;;;;;;EAuFX,WAAA,CAAY,KAAA;EArEV;;;EA+EF,cAAA,CAAA,GAAkB,GAAA,SAAY,UAAA;EAtExB;;;EA6EN,cAAA,CAAA;EA1EW;;;EAiFX,OAAA,CAAQ,IAAA,WAAe,UAAA;EAhFrB;;;EAuFF,KAAA,CAAA;EAAA,QAQQ,kBAAA;EAAA,QAeM,gBAAA;AAAA"}
@@ -1,12 +0,0 @@
1
- //#region src/mocks/nodemailer.d.ts
2
- declare const _default: {
3
- createTransport: () => {
4
- sendMail: () => Promise<{}>;
5
- };
6
- };
7
- declare const createTransport: () => {
8
- sendMail: () => Promise<{}>;
9
- };
10
- //#endregion
11
- export { createTransport, _default as default };
12
- //# sourceMappingURL=nodemailer.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodemailer.d.mts","names":[],"sources":["../../src/mocks/nodemailer.ts"],"mappings":";;;;;;cAMa,eAAA;kBAEX,OAAA;AAAA"}
@@ -1,7 +0,0 @@
1
- //#region src/mocks/nodemailer.ts
2
- var nodemailer_default = { createTransport: () => ({ sendMail: () => Promise.resolve({}) }) };
3
- const createTransport = () => ({ sendMail: () => Promise.resolve({}) });
4
- //#endregion
5
- export { createTransport, nodemailer_default as default };
6
-
7
- //# sourceMappingURL=nodemailer.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodemailer.mjs","names":[],"sources":["../../src/mocks/nodemailer.ts"],"sourcesContent":["export default {\n createTransport: () => ({\n sendMail: () => Promise.resolve({}),\n }),\n}\n\nexport const createTransport = () => ({\n sendMail: () => Promise.resolve({}),\n})\n"],"mappings":";AAAA,IAAA,qBAAe,EACb,wBAAwB,EACtB,gBAAgB,QAAQ,QAAQ,EAAE,CAAC,EACpC,GACF;AAED,MAAa,yBAAyB,EACpC,gBAAgB,QAAQ,QAAQ,EAAE,CAAC,EACpC"}