@ucdjs/test-utils 1.0.1-beta.1

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.
@@ -0,0 +1,210 @@
1
+ import { mockFetch } from "./msw.mjs";
2
+ import { a as parseLatency, c as unsafeResponse, d as defaultArabicShapingFileContent, f as addPathsToFileNodes, i as omitChildrenAndContent, l as defaultDerivedBidClassFileContent, n as MOCK_ROUTES, o as wrapMockFetch, r as extractConfiguredMetadata, s as configure, t as createFileTree, u as defaultBidiBracketsFileContent } from "./file-tree-CColHWG6.mjs";
3
+ import { createDebugger, findFileByPath, isApiError } from "@ucdjs-internal/shared";
4
+ import { UCD_STAT_CHILDREN_DIRS_HEADER, UCD_STAT_CHILDREN_FILES_HEADER, UCD_STAT_CHILDREN_HEADER, UCD_STAT_SIZE_HEADER, UCD_STAT_TYPE_HEADER } from "@ucdjs/env";
5
+ import { HttpResponse } from "msw";
6
+
7
+ //#region src/mock-store/index.ts
8
+ const debug = createDebugger("ucdjs:test-utils:mock-store");
9
+ const DEFAULT_MOCK_STORE_FILES = { "*": [
10
+ {
11
+ type: "file",
12
+ name: "ArabicShaping.txt",
13
+ lastModified: 17552871e5,
14
+ _content: defaultArabicShapingFileContent
15
+ },
16
+ {
17
+ type: "file",
18
+ name: "BidiBrackets.txt",
19
+ lastModified: 17552871e5,
20
+ _content: defaultBidiBracketsFileContent
21
+ },
22
+ {
23
+ type: "directory",
24
+ name: "extracted",
25
+ lastModified: 17552871e5,
26
+ children: [{
27
+ type: "file",
28
+ name: "DerivedBidiClass.txt",
29
+ lastModified: 17552871e5,
30
+ _content: defaultDerivedBidClassFileContent
31
+ }]
32
+ }
33
+ ] };
34
+ function mockStoreApi(config) {
35
+ const { baseUrl = "https://api.ucdjs.dev", responses, versions = [
36
+ "16.0.0",
37
+ "15.1.0",
38
+ "15.0.0"
39
+ ], customResponses = [], onRequest, files = DEFAULT_MOCK_STORE_FILES } = config || {};
40
+ debug?.("Setting up mock store API with config:", config);
41
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
42
+ for (const route of MOCK_ROUTES) {
43
+ const endpoint = route.endpoint;
44
+ const response = responses?.[endpoint] ?? false;
45
+ if (response === false) continue;
46
+ let { actualResponse, latency, headers, beforeHook, afterHook } = extractConfiguredMetadata(response);
47
+ if (typeof actualResponse === "object" && actualResponse !== null && "__useDefaultResolver" in actualResponse) actualResponse = true;
48
+ const shouldUseDefaultValue = actualResponse === true || response == null;
49
+ debug?.(`Setting up mock for endpoint: ${endpoint} with response:`, actualResponse);
50
+ if (isApiError(actualResponse)) {
51
+ debug?.(`Detected ApiError-like response for endpoint: ${endpoint}`);
52
+ let tmp = actualResponse;
53
+ function newHandler() {
54
+ return HttpResponse.json(tmp, { status: tmp.status });
55
+ }
56
+ actualResponse = newHandler;
57
+ }
58
+ const wrappedMockFetch = wrapMockFetch(mockFetch, {
59
+ onRequest,
60
+ beforeFetch: async (payload) => {
61
+ debug?.("Before fetch for endpoint:", endpoint);
62
+ if (latency) {
63
+ const ms = parseLatency(latency);
64
+ await new Promise((resolve) => setTimeout(resolve, ms));
65
+ }
66
+ await beforeHook?.(payload);
67
+ },
68
+ afterFetch: async ({ response, ...payload }) => {
69
+ debug?.("After fetch for endpoint:", endpoint);
70
+ await afterHook?.({
71
+ ...payload,
72
+ response
73
+ });
74
+ if (!response) {
75
+ debug?.("No response returned from resolver");
76
+ return;
77
+ }
78
+ if (!headers || !("headers" in response) || !(response.headers instanceof Headers)) return;
79
+ for (const [key, value] of Object.entries(headers)) if (response.headers.get(key) !== value) response.headers.set(key, value);
80
+ }
81
+ });
82
+ const mswPath = toMSWPath(endpoint);
83
+ debug?.(`MSW path for endpoint ${endpoint}: ${mswPath}`);
84
+ route.setup({
85
+ url: `${normalizedBaseUrl}${mswPath}`,
86
+ providedResponse: actualResponse,
87
+ shouldUseDefaultValue,
88
+ mockFetch: wrappedMockFetch,
89
+ versions,
90
+ files
91
+ });
92
+ }
93
+ if (customResponses.length > 0) {
94
+ debug?.("Setting up custom responses");
95
+ mockFetch(customResponses);
96
+ }
97
+ }
98
+ function toMSWPath(endpoint) {
99
+ return endpoint.replace(/\{(\w+)\}/g, (_, p1) => {
100
+ if (p1 === "wildcard") return `:${p1}*`;
101
+ return `:${p1}`;
102
+ });
103
+ }
104
+ /**
105
+ * Sets up mock handlers for the store subdomain (ucd-store.ucdjs.dev).
106
+ *
107
+ * This is used for the HTTP fs-bridge which directly accesses files via the store subdomain
108
+ * rather than through the API. The store subdomain handles paths like /:version/:filepath
109
+ * without the /ucd/ prefix (it's handled internally by the subdomain).
110
+ *
111
+ * @param {object} config - Configuration for the store subdomain mock
112
+ * @param {string} [config.storeBaseUrl] - Base URL for the store subdomain (defaults to https://ucd-store.ucdjs.dev)
113
+ * @param {MockStoreFiles} config.files - The files to mock
114
+ */
115
+ function mockStoreSubdomain(config) {
116
+ const { storeBaseUrl = "https://ucd-store.ucdjs.dev", files } = config;
117
+ debug?.("Setting up mock store subdomain with config:", config);
118
+ mockFetch([[
119
+ ["GET", "HEAD"],
120
+ `${storeBaseUrl.endsWith("/") ? storeBaseUrl.slice(0, -1) : storeBaseUrl}/:wildcard*`,
121
+ ({ request, params }) => {
122
+ const wildcard = params.wildcard || "";
123
+ const isHeadRequest = request.method === "HEAD";
124
+ debug?.("Store subdomain request:", {
125
+ wildcard,
126
+ method: request.method
127
+ });
128
+ const [firstPart, ...pathParts] = wildcard.split("/");
129
+ const isVersionKey = firstPart && firstPart in files;
130
+ let version = "";
131
+ let filePath;
132
+ let versionFiles = [];
133
+ let lookupPath;
134
+ if (isVersionKey || firstPart && files["*"]) {
135
+ version = firstPart;
136
+ filePath = pathParts.join("/");
137
+ const versionFilesRaw = files[firstPart] || files["*"];
138
+ if (!versionFilesRaw || !Array.isArray(versionFilesRaw)) return HttpResponse.json({
139
+ message: "Resource not found",
140
+ status: 404,
141
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
142
+ }, { status: 404 });
143
+ versionFiles = addPathsToFileNodes(versionFilesRaw, version, "");
144
+ lookupPath = filePath ? `/${version}/${filePath}` : `/${version}`;
145
+ } else {
146
+ filePath = wildcard;
147
+ const rootFilesRaw = files.root;
148
+ if (!rootFilesRaw || !Array.isArray(rootFilesRaw)) return HttpResponse.json({
149
+ message: "Resource not found",
150
+ status: 404,
151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
152
+ }, { status: 404 });
153
+ versionFiles = addPathsToFileNodes(rootFilesRaw, "", "");
154
+ lookupPath = filePath ? `/${filePath}` : "/";
155
+ }
156
+ debug?.("Looking up path:", {
157
+ lookupPath,
158
+ versionFilesCount: versionFiles.length
159
+ });
160
+ if (version && !filePath) {
161
+ const stripped = omitChildrenAndContent(versionFiles);
162
+ return HttpResponse.json(stripped, { headers: {
163
+ [UCD_STAT_TYPE_HEADER]: "directory",
164
+ [UCD_STAT_CHILDREN_HEADER]: `${stripped.length}`,
165
+ [UCD_STAT_CHILDREN_FILES_HEADER]: `${stripped.filter((f) => f.type === "file").length}`,
166
+ [UCD_STAT_CHILDREN_DIRS_HEADER]: `${stripped.filter((f) => f.type === "directory").length}`
167
+ } });
168
+ }
169
+ const fileNode = findFileByPath(versionFiles, lookupPath);
170
+ if (fileNode && fileNode.type === "directory") {
171
+ const stripped = omitChildrenAndContent(fileNode.children ?? []);
172
+ return HttpResponse.json(stripped, { headers: {
173
+ [UCD_STAT_TYPE_HEADER]: "directory",
174
+ [UCD_STAT_CHILDREN_HEADER]: `${stripped.length}`,
175
+ [UCD_STAT_CHILDREN_FILES_HEADER]: `${stripped.filter((f) => f.type === "file").length}`,
176
+ [UCD_STAT_CHILDREN_DIRS_HEADER]: `${stripped.filter((f) => f.type === "directory").length}`
177
+ } });
178
+ }
179
+ if (fileNode && "_content" in fileNode && typeof fileNode._content === "string") {
180
+ const content = fileNode._content;
181
+ const contentLength = new TextEncoder().encode(content).length;
182
+ const headers = {
183
+ "Content-Type": "text/plain; charset=utf-8",
184
+ [UCD_STAT_TYPE_HEADER]: "file"
185
+ };
186
+ if (isHeadRequest) {
187
+ headers["Content-Length"] = `${contentLength}`;
188
+ headers[UCD_STAT_SIZE_HEADER] = `${contentLength}`;
189
+ }
190
+ return HttpResponse.text(content, { headers });
191
+ }
192
+ if (fileNode) {
193
+ console.warn(`Mock store subdomain: File "${filePath}" found but has no _content. Returning 404.`);
194
+ return HttpResponse.json({
195
+ message: "Resource not found",
196
+ status: 404,
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
198
+ }, { status: 404 });
199
+ }
200
+ return HttpResponse.json({
201
+ message: "Resource not found",
202
+ status: 404,
203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
204
+ }, { status: 404 });
205
+ }
206
+ ]]);
207
+ }
208
+
209
+ //#endregion
210
+ export { configure, createFileTree, mockStoreApi, mockStoreSubdomain, unsafeResponse };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,14 @@
1
+ import { mswServer } from "../msw.mjs";
2
+ import { afterAll, afterEach, beforeAll } from "vitest";
3
+
4
+ //#region src/msw/vitest-setup.ts
5
+ beforeAll(() => {
6
+ mswServer.listen({ onUnhandledRequest: (_, print) => {
7
+ print.error();
8
+ } });
9
+ });
10
+ afterEach(() => mswServer.resetHandlers());
11
+ afterAll(() => mswServer.close());
12
+
13
+ //#endregion
14
+ export { };
package/dist/msw.d.mts ADDED
@@ -0,0 +1,25 @@
1
+ import { HttpResponse, http } from "msw";
2
+ import * as _luxass_msw_utils0 from "@luxass/msw-utils";
3
+ import { createMockFetch } from "@luxass/msw-utils";
4
+ import { SetupServerApi } from "msw/node";
5
+
6
+ //#region src/msw.d.ts
7
+ declare global {
8
+ var __ucd_msw_server: SetupServerApi | undefined;
9
+ }
10
+ declare const mswServer: SetupServerApi;
11
+ declare const mockFetch: _luxass_msw_utils0.MockFetchFn;
12
+ /**
13
+ * A `Response` subclass that avoids default headers.
14
+ * Useful for MSW or unit tests to simulate "naked" responses from a server
15
+ * that lack a `Content-Type` header, allowing you to test fallback parsing logic.
16
+ */
17
+ declare class RawResponse extends Response {
18
+ /**
19
+ * @param {BodyInit | null} body - The body of the response.
20
+ * @param {ResponseInit} [init] - Options including status code and headers.
21
+ */
22
+ constructor(body?: BodyInit | null, init?: ResponseInit);
23
+ }
24
+ //#endregion
25
+ export { HttpResponse, RawResponse, createMockFetch, http, mockFetch, mswServer };
package/dist/msw.mjs ADDED
@@ -0,0 +1,30 @@
1
+ import { HttpResponse, http } from "msw";
2
+ import { createMockFetch, createMockFetch as createMockFetch$1 } from "@luxass/msw-utils";
3
+ import { setupServer } from "msw/node";
4
+
5
+ //#region src/msw.ts
6
+ const mswServer = setupServer();
7
+ globalThis.__ucd_msw_server = mswServer;
8
+ const mockFetch = createMockFetch$1({
9
+ mswServer,
10
+ replaceOpenAPIPathParams: true
11
+ });
12
+ /**
13
+ * A `Response` subclass that avoids default headers.
14
+ * Useful for MSW or unit tests to simulate "naked" responses from a server
15
+ * that lack a `Content-Type` header, allowing you to test fallback parsing logic.
16
+ */
17
+ var RawResponse = class extends Response {
18
+ /**
19
+ * @param {BodyInit | null} body - The body of the response.
20
+ * @param {ResponseInit} [init] - Options including status code and headers.
21
+ */
22
+ constructor(body, init) {
23
+ let processedBody = body;
24
+ if (typeof body === "string") processedBody = new TextEncoder().encode(body);
25
+ super(processedBody, init);
26
+ }
27
+ };
28
+
29
+ //#endregion
30
+ export { HttpResponse, RawResponse, createMockFetch, http, mockFetch, mswServer };
@@ -0,0 +1,21 @@
1
+ //#region src/pipelines/source.d.ts
2
+ interface PipelineDefinition {
3
+ _type?: string;
4
+ id: string;
5
+ versions?: string[];
6
+ inputs?: unknown[];
7
+ routes?: unknown[];
8
+ [key: string]: unknown;
9
+ }
10
+ type NamedExportValue = PipelineDefinition | string;
11
+ type NamedExportConfig = Record<string, NamedExportValue>;
12
+ type PipelineModuleSourceNamed = string[] | NamedExportConfig;
13
+ interface PipelineModuleSourceOptions {
14
+ named?: PipelineModuleSourceNamed;
15
+ definitions?: Record<string, Partial<PipelineDefinition>>;
16
+ extraExports?: string;
17
+ prelude?: string;
18
+ }
19
+ declare function createPipelineModuleSource(options?: PipelineModuleSourceOptions): string;
20
+ //#endregion
21
+ export { type PipelineDefinition, type PipelineModuleSourceOptions, createPipelineModuleSource };
@@ -0,0 +1,35 @@
1
+ //#region src/pipelines/source.ts
2
+ function isStringArray(value) {
3
+ return Array.isArray(value);
4
+ }
5
+ function isPipelineDefinition(value) {
6
+ return typeof value === "object" && value !== null && "id" in value;
7
+ }
8
+ function buildDefinition(id, overrides) {
9
+ const def = {
10
+ _type: "pipeline-definition",
11
+ id,
12
+ versions: ["16.0.0"],
13
+ inputs: [],
14
+ routes: [],
15
+ ...overrides
16
+ };
17
+ return JSON.stringify(def).replace(/"(\w+)":/g, "$1:");
18
+ }
19
+ function buildNamedExports(named, definitions) {
20
+ if (isStringArray(named)) return named.map((name) => `export const ${name} = ${buildDefinition(name, definitions[name])};`).join("\n\n");
21
+ return Object.entries(named).map(([name, value]) => {
22
+ return `export const ${name} = ${isPipelineDefinition(value) ? buildDefinition(value.id, value) : value};`;
23
+ }).join("\n\n");
24
+ }
25
+ function createPipelineModuleSource(options = {}) {
26
+ const { named = [], definitions = {}, extraExports, prelude } = options;
27
+ return [
28
+ prelude,
29
+ buildNamedExports(named, definitions),
30
+ extraExports
31
+ ].filter(Boolean).join("\n\n");
32
+ }
33
+
34
+ //#endregion
35
+ export { createPipelineModuleSource };
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@ucdjs/test-utils",
3
+ "version": "1.0.1-beta.1",
4
+ "type": "module",
5
+ "author": {
6
+ "name": "Lucas Nørgård",
7
+ "email": "lucasnrgaard@gmail.com",
8
+ "url": "https://luxass.dev"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/ucdjs/ucd",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ucdjs/ucd.git",
15
+ "directory": "packages/test-utils"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/ucdjs/ucd/issues"
19
+ },
20
+ "exports": {
21
+ ".": "./dist/index.mjs",
22
+ "./fs-bridges": "./dist/fs-bridges/index.mjs",
23
+ "./matchers/types": "./dist/matchers/types.mjs",
24
+ "./matchers/vitest-setup": "./dist/matchers/vitest-setup.mjs",
25
+ "./mock-store": "./dist/mock-store.mjs",
26
+ "./msw": "./dist/msw.mjs",
27
+ "./msw/vitest-setup": "./dist/msw/vitest-setup.mjs",
28
+ "./pipelines": "./dist/pipelines.mjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "types": "./dist/index.d.mts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22.18"
37
+ },
38
+ "peerDependencies": {
39
+ "vitest-testdirs": "4.4.2",
40
+ "@ucdjs/ucd-store": "1.0.1-beta.1"
41
+ },
42
+ "dependencies": {
43
+ "@luxass/msw-utils": "0.6.0",
44
+ "@luxass/utils": "2.7.3",
45
+ "@unicode-utils/core": "0.12.0-beta.19",
46
+ "msw": "2.12.10",
47
+ "zod": "4.3.6",
48
+ "@ucdjs-internal/shared": "0.1.1-beta.1",
49
+ "@ucdjs/fs-bridge": "0.1.1-beta.1",
50
+ "@ucdjs/schemas": "0.1.1-beta.1",
51
+ "@ucdjs/env": "0.1.1-beta.1"
52
+ },
53
+ "devDependencies": {
54
+ "@luxass/eslint-config": "7.2.0",
55
+ "@vitest/expect": "4.1.0-beta.1",
56
+ "eslint": "10.0.0",
57
+ "openapi-typescript": "7.13.0",
58
+ "publint": "0.3.17",
59
+ "tsdown": "0.20.3",
60
+ "tsx": "4.21.0",
61
+ "typescript": "5.9.3",
62
+ "vitest-testdirs": "4.4.2",
63
+ "@ucdjs-tooling/tsdown-config": "1.0.0",
64
+ "@ucdjs/ucd-store": "1.0.1-beta.1",
65
+ "@ucdjs-tooling/tsconfig": "1.0.0"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "scripts": {
71
+ "build": "tsdown --tsconfig=./tsconfig.build.json",
72
+ "dev": "tsdown --watch",
73
+ "clean": "git clean -xdf dist node_modules",
74
+ "lint": "eslint .",
75
+ "typecheck": "tsc --noEmit -p tsconfig.build.json",
76
+ "generate:client": "openapi-typescript ../../ucd-generated/api/openapi.json -o ./src/.generated/api.d.ts",
77
+ "generate:client:local": "openapi-typescript http://localhost:8787/openapi.json -o ./src/.generated/api.d.ts"
78
+ }
79
+ }