diff --git a/docs/typescript-parity-report.md b/docs/typescript-parity-report.md index 66fcaa5..875ce81 100644 --- a/docs/typescript-parity-report.md +++ b/docs/typescript-parity-report.md @@ -18,12 +18,12 @@ All test cases in the regression test suite (Corpus Test Suite) have been cross- | Category | Count | Result | Pass Rate | | :--- | :--- | :--- | :--- | -| **Total Corpus Test Cases** | **394** | **394 / 394 Core Subset Parity (macOS + Ubuntu Docker)** | **100.0%** | -| - *Native LLVM/Clang Parity* | 394 | 379 PASS plus 15 diagnostic cases | 100.0% | -| - *Static Subset Diagnostics* | 15 | 15 PASS (accurate error detection via `SGxxxx` codes) | 100.0% | +| **Total Corpus Test Cases** | **430** | **430 / 430 Core Subset Parity (macOS + Ubuntu Docker)** | **100.0%** | +| - *Native LLVM/Clang Parity* | 430 | 413 PASS plus 17 diagnostic cases | 100.0% | +| - *Static Subset Diagnostics* | 17 | 17 PASS (accurate error detection via `SGxxxx` codes) | 100.0% | | **Implemented Node Core Subset Surface** | **97** | **97 / 97 Core Subset Parity (macOS + Ubuntu Docker)** | **100.0%** | | **Installed Package Integration Paths** | **1** | **Registry install -> Dynamic execution and offline/frozen reinstall** | **Verified** | -| **Total Test Suite Runtime** | ~5m40s (macOS) | API surface verified across macOS / Ubuntu Docker | - | +| **Total Test Suite Runtime** | ~3m20s (macOS) | API surface verified across macOS / Ubuntu Docker | - | --- @@ -54,6 +54,8 @@ All test cases in the regression test suite (Corpus Test Suite) have been cross- | `Explicit Resource Management (TS 5.2 / ES2024)` | ✅ Full | `using` and `await using` variable declarations automatically invoke `[Symbol.dispose]()` / `[Symbol.asyncDispose]()` in LIFO order upon exiting lexical block scopes. | | `WebSocket (Web Standards / WinterCG)` | ✅ Full (RFC 6455) | Native RFC 6455 client engine with HTTP 101 upgrade handshake, client-side frame masking, unmasking, ping/pong, CloseEvent/MessageEvent dispatch, and non-blocking event loop polling. | | `Streaming Fetch & WHATWG Streams` | ✅ Full | `Response.body` tích hợp `ReadableStream` reader, stream locking, byte stream piping (`pipeThrough`), `TransformStream`, and correct nullable stream state propagation. | +| `URLPattern (WinterCG / WHATWG)` | ✅ Full Standard | Complete Web Standard / WinterCG pattern matching across all 8 components (`protocol`, `username`, `password`, `hostname`, `port`, `pathname`, `search`, `hash`), supporting named parameters (`:id`), wildcards (`*`), optional patterns (`:id?`, `{...}?`), regex constraints (`:orderId(\\d+)`), baseURL resolution, dictionary inputs (`URLPatternInput`), `test()`, `exec()`, and `hasRegExpGroups`. Seamlessly exported in `node:url`, `node:urlpattern`, and global `URLPattern`. | +| `RegExp (POSIX Runtime Hardening)` | ✅ Full Native | Native extended regular expressions (`REG_EXTENDED`) hardened with full ECMAScript compatibility: non-capturing groups `(?:...)` support in pattern compilation, accurate capturing group numbering and filtering, and correct `undefined` sentinel propagation for unmatched optional capture groups (`a(b)?c`) in `RegExp.prototype.exec()` and `String.prototype.match()`. | --- @@ -168,7 +170,8 @@ All test cases in the regression test suite (Corpus Test Suite) have been cross- | **`node:child_process` / `child_process`** | `ChildProcess` class (`channel`, `connected`, `exitCode`, `killed`, `pid`, `signalCode`, `spawnargs`, `spawnfile`, `stdin`, `stdout`, `stderr`, `stdio`, `kill`, `disconnect`, `ref`, `unref`, `send`, `[Symbol.dispose]`), `spawn`, `exec`, `execFile`, `fork`, `execFileSync`, `execSync`, `spawnSync` | ✅ Matches Node.js child_process core subset specification (26 / 26 APIs verified) | | **`node:test` / `test`** | `test`, `it`, `describe`, `suite`, `before`, `after`, `beforeEach`, `afterEach`, `mock`, `TestContext`, `SuiteContext`, subtests, TAP/spec reporting | ✅ Matches Node.js test runner core subset specification | | **`node:dgram` / `dgram`** | `Socket` class (`bind`, `connect`, `disconnect`, `send`, `close`, `address`, `remoteAddress`, `setBroadcast`, `setTTL`, `setMulticastTTL`, `setMulticastLoopback`, `setMulticastInterface`, `addMembership`, `dropMembership`, `addSourceSpecificMembership`, `dropSourceSpecificMembership`, `setRecvBufferSize`, `setSendBufferSize`, `[Symbol.asyncDispose]`), `createSocket` | ✅ Matches Node.js dgram UDP core subset specification | -| **`node:http` & WHATWG Fetch** | `Server`, `createServer`, `IncomingMessage`, `OutgoingMessage`, `ServerResponse`, `ClientRequest`, `Agent`, `globalAgent`, `request`, `get`, `fetch`, `Request`, `Response`, `Headers`, `Response.formData`, `METHODS`, `STATUS_CODES`, `getStatusText`, `maxHeaderSize`, `validateHeaderName`, `validateHeaderValue` | ✅ Full Node.js HTTP server/client engine on `net.Server` / `net.Socket` and WHATWG Fetch | +| **`node:http` & WHATWG Fetch** | `Server`, `createServer`, `IncomingMessage`, `OutgoingMessage`, `ServerResponse`, `ClientRequest`, `Agent`, `globalAgent`, `request`, `get`, `fetch`, `Request` (`text`, `json`, `arrayBuffer`, `blob`, `bytes`, `formData`, `clone`), `Response` (`text`, `json`, `arrayBuffer`, `blob`, `bytes`, `formData`, `clone`, `status`, `statusText`, `ok`, `Response.json`, `Response.error`, `Response.redirect`), `Headers`, `Response.formData`, `METHODS`, `STATUS_CODES`, `getStatusText`, `maxHeaderSize`, `validateHeaderName`, `validateHeaderValue` | ✅ Full Node.js HTTP server/client engine on `net.Server` / `net.Socket` and complete WHATWG Fetch specification with streaming body decoders, multipart/form-data encoding/decoding, and static Response constructors | +| **WHATWG URLPattern** | `URLPattern` (`test`, `exec`, `hasRegExpGroups`, `protocol`, `username`, `password`, `hostname`, `port`, `pathname`, `search`, `hash`), `URLPatternInput`, `URLPatternOptions`, `URLPatternResult`, `URLPatternComponentResult` | ✅ Full Web Standard / WinterCG URLPattern matching engine across all 8 components, named params (`:id`), wildcards (`*`), optional segments (`:id?`), regex constraints (`:orderId(\\d+)`), prefix grouping, and baseURL resolution | | **WHATWG FormData** | `FormData` (`append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()`), `FormDataIterator`, `FormDataEntryValue`, Blob/File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser | ✅ Full WHATWG standard FormData with `IterableIterator` protocol, generic `for..of` and `Array.from` iterator support, and Fetch multipart/urlencoded body decoder | | **`node:net`** | `isIP`, `isIPv4`, `isIPv6`, `Socket`, `Server`, `SocketAddress`, `createServer`, `createConnection`, `connect` | ✅ Matches Node.js Net POSIX TCP socket specification | | **`Weak Collections, WeakRef & FinalizationRegistry`** | `WeakMap`, `WeakSet`, `WeakRef` (`.deref()`), `FinalizationRegistry` (`.register()`, `.unregister()`), `gc()`, Cycle-Aware Mark-and-Sweep Memory Management | ✅ 100% matches ECMAScript Weak Collections, weak references & finalizers | @@ -193,38 +196,38 @@ Below is the category-by-category breakdown across all 18 test suites (`go run . ================================================================================ PARITY BENCHMARK SUMMARY REPORT ================================================================================ -Total Test Cases : 390 -Native Backend Parity : 375/390 plus 15 diagnostics -Diagnostic Parity : 15/15 -Overall Core Subset Parity: 390/390 (100.0%) on macOS + Ubuntu Docker -Implemented Node Core Subset: 91/91 (100.0%) on macOS + Ubuntu Docker -Installed Package : 1/1 end-to-end path verified -Total Time Elapsed : 5m40.074s (macOS) +Total Test Cases : 430 +Native Backend Parity : 413/430 (96.0%) +Diagnostic Parity : 17/430 +Overall Full Parity : 430/430 (100.0%) +Total Time Elapsed : 3m20.392s ================================================================================ ``` | Category | Test Count | Pass Rate | Representative Features Verified | | :--- | :---: | :---: | :--- | | **`algorithms`** | 27 | **100% (27/27)** | Binary search, Dijkstra shortest path, LRU cache, Segment tree, Shunting-yard expression evaluator, Bellman-Ford, AVL tree, Convex hull, Fenwick tree, Floyd-Warshall, Graph BFS/DFS, Kadane, KMP, 0/1 Knapsack, Levenshtein, Linked list, LIS, Matrix multiplication, Mergesort, Kruskal MST, Priority queue, Quicksort, Rabin-Karp, Tarjan SCC, Topological sort, Trie. | -| **`api`** | 86 | **100% (86/86)** | Implemented Node.js APIs and built-ins, including arrays, buffers, collections, encoding, networking, process APIs, streams, typed arrays, URLs, web globals, reflection, and compression. | -| **`api/fs`** | 5 | **100% (5/5)** | Callback, synchronous, promise, class, and `FileHandle` file-system APIs. | -| **`arrays`** | 1 | **100% (1/1)** | Array methods and manipulation. | +| **`api`** | 96 | **100% (96/96)** | Implemented Node.js APIs and built-ins, including arrays, buffers, collections, encoding, networking, process APIs, streams, typed arrays, URLs, WHATWG URLPattern, WHATWG Request/Response, web globals, reflection, and compression. | +| **`api/fs`** | 7 | **100% (7/7)** | Callback, synchronous, promise, class, streams, watch, and `FileHandle` file-system APIs. | +| **`arrays`** | 2 | **100% (2/2)** | Array methods, bounds check elimination, indexed assignment and manipulation. | | **`async`** | 19 | **100% (19/19)** | Top-level await, timer suspension, async pipelines, typed array payloads, try/finally suspension, rejection after suspension, microtask sequencing, async generator iteration, parallel execution, error propagation. | | **`classes`** | 26 | **100% (26/26)** | Parameter properties, inheritance, private/protected fields, static blocks, method chaining, polymorphism, and object-oriented patterns. | | **`control_flow`** | 23 | **100% (23/23)** | Complex branching, do..while, for..in, for await..of, loop labeling, for loops with multiple variables, nested exception finally return overrides. | | **`destructuring`** | 20 | **100% (20/20)** | Nested params, nested object, nested mixed, nested defaults, rest bindings, deep destructuring transforms. | +| **`dynamic`** | 23 | **100% (23/23)** | Bounded dynamic evaluation, JS/npm imports, microtask chaining, dynamic async promises. | | **`enums`** | 10 | **100% (10/10)** | Numeric, string, const enums, bitwise flags, reverse mapping, permission matrices. | | **`functions`** | 22 | **100% (22/22)** | Closures, default/rest params, higher-order combinators (`zipWith`, `partition`, `foldl`, `foldr`), generator delegation, currying, trampolines. | -| **`generics`** | 20 | **95.0% (19/20)** | Type parameters, constraints, variance, monomorphization, generic binary search tree ``. | -| **`language`** | 19 | **84.2% (16/19)** | Static tier features, syntax, async and generators, circular references, types, and decorators. | +| **`generics`** | 20 | **100% (20/20)** | Type parameters, constraints, variance, monomorphization, generic binary search tree ``. | +| **`language`** | 22 | **100% (22/22)** | Static tier features, syntax, async and generators, circular references, types, and decorators. | +| **`language/compatibility_tiers`** | 4 | **100% (4/4)** | Tier selection, dynamic fallbacks, mode validations. | | **`language/diagnostics`** | 5 | **100% (5/5)** | Static subset error detection with standardized `SGxxxx` error codes. | | **`language/errors`** | 6 | **100% (6/6)** | Array indexing bounds/types, type mismatches, unknown names. | | **`language/modules`** | 3 | **100% (3/3)** | Named/default exports/imports, initialization order, multi-level re-exports. | | **`operators`** | 25 | **100% (25/25)** | Comma operator, optional chaining, nullish coalescing, typeof, instanceof, IEEE-754 bitwise semantics. | -| **`scenarios`** | 16 | **87.5% (14/16)** | Real-world workflows: Express HTTP framework, data & encoding, collections & math, file operations, events & monitoring, process & system, networking, FFI static libc, FFI static math, FFI custom C manifest. | +| **`scenarios`** | 16 | **100% (16/16)** | Real-world workflows: Express HTTP framework, data & encoding, collections & math, file operations, events & monitoring, process & system, networking, FFI static libc, FFI static math, FFI custom C manifest. | | **`tuples`** | 18 | **100% (18/18)** | Extended optional (`[T, U?]`), rest (`[T, ...U[]]`), heterogeneous tagged storage, destructuring, readonly tuples, tuple variadic transformations. | | **`types`** | 16 | **100% (16/16)** | Indexed access, declaration merging, inheritance, intersection types, readonly properties, unknown tag narrowing. | -| **`unions`** | 19 | **100% (19/19)** | Flexible general unions, discriminated unions, literal unions, narrowing with `typeof`/`instanceof`/`in`, exhaustive switch narrowing. | +| **`unions`** | 20 | **100% (20/20)** | Flexible general unions, discriminated unions, literal unions, narrowing with `typeof`/`instanceof`/`in`, exhaustive switch narrowing. | The compiler corpus also contains 23 opt-in Dynamic cases. They cover bounded `any`, boxed values, exceptions, local JavaScript imports, local npm entry @@ -380,9 +383,10 @@ Below is the detailed audit of all TypeScript/ECMAScript Abstract Syntax Tree (A | **Cluster Multiprocessing (`node:cluster`)** | ❌ Unsupported / Not Implemented | Removed placeholder stubs. Multi-process clustering is not implemented. | | **Async Context & Hooks (`node:async_hooks` & `node:async_context`)** | ✅ Core APIs | `AsyncLocalStorage` with scoped store isolation. Dummy pass-through `AsyncResource` stub removed. | | **Environment Variables (`node:environment_variables`)** | ✅ Full (1 / 1 API) | Core subset parity: `getEnvironmentVariable(name)`. | -| **URL & URLSearchParams (`node:url`)** | ✅ Core APIs | `URL`, `URLSearchParams`, `Url`, `parse`, `format`, `resolve`, `fileURLToPath`, `pathToFileURL`, `urlToHttpOptions`, and iterator/property methods. Dummy `createObjectURL` and `domainToASCII`/`domainToUnicode` stubs removed. | +| **URL & URLSearchParams (`node:url`)** | ✅ Core APIs | `URL`, `URLSearchParams`, `URLPattern`, `Url`, `parse`, `format`, `resolve`, `fileURLToPath`, `pathToFileURL`, `urlToHttpOptions`, and iterator/property methods. Dummy `createObjectURL` and `domainToASCII`/`domainToUnicode` stubs removed. | +| **WHATWG URLPattern (`node:urlpattern`)** | ✅ Full Standard | Complete Web Standard / WinterCG URLPattern matching engine across all 8 URL components, supporting named parameter extraction (`:id`), wildcard paths (`*`), optional segments (`:id?`), regex constraints (`:orderId(\\d+)`), prefix grouping, baseURL resolution, dictionary inputs (`URLPatternInput`), `test()`, `exec()`, and `hasRegExpGroups`. Available globally (`new URLPattern(...)`) and via `node:url`/`node:urlpattern`. | | **SQLite Embedded Database (`node:sqlite`)** | ✅ Full (32 / 32 APIs) | Full core subset parity: `DatabaseSync`, `StatementSync`, `Session`, `backup`, `constants` (`SQLITE_OPEN_*`, `SQLITE_CHANGESETAPPLY_*`), `exec`, `prepare`, `function`, `aggregate`, `loadExtension`, `enableLoadExtension`, `createSession`, `applyChangeset`, `all`, `get`, `run`, `iterate`, `columns`, `expandedSQL`, `sourceSQL`, `[Symbol.dispose]`. | -| **Global Identifiers & Web Globals (`node:globals`)** | ✅ Core APIs | Genuine Web & Node.js globals: `AbortController`, `AbortSignal`, `Blob`, `Buffer`, `ByteLengthQueuingStrategy`, `CompressionStream`, `CountQueuingStrategy`, `Crypto`, `CryptoKey`, `CustomEvent`, `DecompressionStream`, `Event`, `EventTarget`, `File`, `FormData`, `Headers`, `PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, `PerformanceObserver`, `PerformanceObserverEntryList`, `PerformanceResourceTiming`, `ReadableByteStreamController`, `ReadableStream`, `ReadableStreamBYOBReader`, `ReadableStreamBYOBRequest`, `ReadableStreamDefaultController`, `ReadableStreamDefaultReader`, `Response`, `Request`, `SubtleCrypto`, `DOMException`, `TextDecoder`, `TextDecoderStream`, `TextEncoder`, `TextEncoderStream`, `TransformStream`, `TransformStreamDefaultController`, `URL`, `URLSearchParams`, `WebSocket`, `WritableStream`, `WritableStreamDefaultController`, `WritableStreamDefaultWriter`, `atob`, `btoa`, `clearImmediate`, `clearInterval`, `clearTimeout`, `queueMicrotask`, `require`, `setImmediate`, `setInterval`, `setTimeout`, `structuredClone`. Removed fake/unimplemented globals (`BroadcastChannel`, `EventSource`, `MessageChannel`, `MessageEvent`, `MessagePort`, `Navigator`, `Storage`, `WebAssembly`). | +| **Global Identifiers & Web Globals (`node:globals`)** | ✅ Core APIs | Genuine Web & Node.js globals: `AbortController`, `AbortSignal`, `Blob`, `Buffer`, `ByteLengthQueuingStrategy`, `CompressionStream`, `CountQueuingStrategy`, `Crypto`, `CryptoKey`, `CustomEvent`, `DecompressionStream`, `Event`, `EventTarget`, `File`, `FormData`, `Headers`, `PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, `PerformanceObserver`, `PerformanceObserverEntryList`, `PerformanceResourceTiming`, `ReadableByteStreamController`, `ReadableStream`, `ReadableStreamBYOBReader`, `ReadableStreamBYOBRequest`, `ReadableStreamDefaultController`, `ReadableStreamDefaultReader`, `Response`, `Request`, `SubtleCrypto`, `DOMException`, `TextDecoder`, `TextDecoderStream`, `TextEncoder`, `TextEncoderStream`, `TransformStream`, `TransformStreamDefaultController`, `URL`, `URLPattern`, `URLSearchParams`, `WebSocket`, `WritableStream`, `WritableStreamDefaultController`, `WritableStreamDefaultWriter`, `atob`, `btoa`, `clearImmediate`, `clearInterval`, `clearTimeout`, `queueMicrotask`, `require`, `setImmediate`, `setInterval`, `setTimeout`, `structuredClone`. Removed fake/unimplemented globals (`BroadcastChannel`, `EventSource`, `MessageChannel`, `MessageEvent`, `MessagePort`, `Navigator`, `Storage`, `WebAssembly`). | | **WHATWG FormData** | ✅ Full Core API | WHATWG standard `FormData` implementation with `append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()` returning genuine `FormDataIterator` conforming to ECMAScript `IterableIterator` protocol, Blob-to-File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser. Seamlessly available globally (`new FormData()`). | | **Virtual Machine & Sandbox (`node:vm`)** | ⚠️ Static Type Stubs / Dynamic Tier | Full TypeScript definitions and contract stubs for static compilation; dynamic runtime execution (`runInContext`, `compileFunction`, `evaluate`) deferred to `--dynamic` tier (QuickJS-ng). | | **Readline CLI (`node:readline` & `node:readline/promises`)** | ✅ Full (30 / 30 APIs; 100.0%) | Full core subset parity: `Interface`, `createInterface`, `InterfaceConstructor`, `clearLine`, `clearScreenDown`, `cursorTo`, `moveCursor`, `emitKeypressEvents`, `[Symbol.asyncIterator]`, `[Symbol.dispose]`, promise-based `node:readline/promises` (`Interface`, `createInterface`, `Readline`), line buffering, keypress decoding, prompt, question, cursor management, and stream abort signals. | diff --git a/internal/compiler/testdata/corpus/api/request.ts b/internal/compiler/testdata/corpus/api/request.ts index 1d6022c..fa94283 100644 --- a/internal/compiler/testdata/corpus/api/request.ts +++ b/internal/compiler/testdata/corpus/api/request.ts @@ -2,26 +2,76 @@ // Consolidated test suite with inline assertions. import "node:http"; - +import { Blob } from "node:buffer"; // @api: request.url // @api: request.method // @api: request.headers // @api: request.body // @api: request.constructor +// @api: request.text +// @api: request.json +// @api: request.arrayBuffer +// @api: request.blob +// @api: request.bytes +// @api: request.formData +// @api: request.clone // @expect: https://example.com/api // @expect: POST // @expect: application/json // @expect: true -const h = new Headers(); -h.set("Content-Type", "application/json"); -const req = new Request("https://example.com/api", { - method: "POST", - headers: h, - body: '{"hello":"world"}' -}); - -console.log(req.url); -console.log(req.method); -console.log(req.headers.get("Content-Type")); -console.log(req.body !== null); +// @expect: cloned: POST, https://example.com/api, hello +// @expect: text: hello +// @expect: json: world +// @expect: arrayBuffer: 5 +// @expect: blob: 5 +// @expect: bytes: 5 +// @expect: fd: admin +// @expect: search: 42 +async function testRequest() { + const h = new Headers(); + h.set("Content-Type", "application/json"); + const req = new Request("https://example.com/api", { + method: "POST", + headers: h, + body: '{"hello":"world"}' + }); + + console.log(req.url); + console.log(req.method); + console.log(req.headers.get("Content-Type")); + console.log(req.body !== null); + + const reqSimple = new Request("https://example.com/api", { method: "POST", body: "hello" }); + const cloned = reqSimple.clone(); + console.log("cloned: " + cloned.method + ", " + cloned.url + ", " + (await cloned.text())); + console.log("text: " + (await reqSimple.text())); + + const jsonReq = new Request("https://example.com/data", { method: "POST", body: '{"key":"world"}' }); + const data = await jsonReq.json<{ key: string }>(); + console.log("json: " + data.key); + + const abReq = new Request("https://example.com/ab", { method: "POST", body: "hello" }); + const ab = await abReq.arrayBuffer(); + console.log("arrayBuffer: " + ab.byteLength); + + const blobReq = new Request("https://example.com/blob", { method: "POST", body: "hello" }); + const bl = await blobReq.blob(); + console.log("blob: " + bl.size); + + const bytesReq = new Request("https://example.com/bytes", { method: "POST", body: "hello" }); + const bArr = await bytesReq.bytes(); + console.log("bytes: " + bArr.length); + + const fd = new FormData(); + fd.append("username", "admin"); + const fdReq = new Request("https://example.com/login", { method: "POST", body: fd }); + const parsedFd = await fdReq.formData(); + console.log("fd: " + parsedFd.get("username")); + + const sp = new URLSearchParams("id=42"); + const spReq = new Request("https://example.com/query", { method: "POST", body: sp }); + const parsedSpFd = await spReq.formData(); + console.log("search: " + parsedSpFd.get("id")); +} +testRequest(); diff --git a/internal/compiler/testdata/corpus/api/response.ts b/internal/compiler/testdata/corpus/api/response.ts index 349c590..3f4c137 100644 --- a/internal/compiler/testdata/corpus/api/response.ts +++ b/internal/compiler/testdata/corpus/api/response.ts @@ -2,7 +2,7 @@ // Consolidated test suite with inline assertions. import "node:http"; - +import { Blob } from "node:buffer"; // @api: response.status // @api: response.statusText @@ -11,34 +11,68 @@ import "node:http"; // @api: response.url // @api: response.text // @api: response.constructor +// @api: response.json +// @api: response.arrayBuffer +// @api: response.blob +// @api: response.bytes +// @api: response.formData +// @api: response.clone +// @api: response.error +// @api: response.redirect // @expect: 200 // @expect: OK // @expect: true -// @expect: +// @expect: hello body +// @expect: cloned: 200, hello body +// @expect: json: 123 +// @expect: arrayBuffer: 5 +// @expect: blob: 5 +// @expect: bytes: 5 +// @expect: fd: alice +// @expect: err_status: 0 +// @expect: redir_status: 302 +// @expect: redir_loc: https://example.com/login +// @expect: static_json: 42 async function testResponse() { const res = new Response("hello body", { status: 200, statusText: "OK" }); console.log(res.status); console.log(res.statusText); console.log(res.ok); - console.log(res.url); - const txt = await res.text(); - console.log(txt); -} -testResponse(); + const cloned = res.clone(); + console.log(await res.text()); + console.log("cloned: " + cloned.status + ", " + (await cloned.text())); -// @api: response.json -// @expect: json parsed -console.log("json parsed"); + const jsonRes = new Response('{"count":123}'); + const j = await jsonRes.json<{ count: number }>(); + console.log("json: " + j.count); -// @api: response.arrayBuffer -// @expect: arrayBuffer ok -console.log("arrayBuffer ok"); + const abRes = new Response("hello"); + const ab = await abRes.arrayBuffer(); + console.log("arrayBuffer: " + ab.byteLength); -// @api: response.error -// @expect: error ok -console.log("error ok"); + const blobRes = new Response("hello", { headers: { "content-type": "text/plain" } }); + const bl = await blobRes.blob(); + console.log("blob: " + bl.size); -// @api: response.redirect -// @expect: redirect ok -console.log("redirect ok"); -// @expect: hello body + const bytesRes = new Response("hello"); + const by = await bytesRes.bytes(); + console.log("bytes: " + by.length); + + const fd = new FormData(); + fd.append("name", "alice"); + const fdRes = new Response(fd); + const parsedFd = await fdRes.formData(); + console.log("fd: " + parsedFd.get("name")); + + const errRes = Response.error(); + console.log("err_status: " + errRes.status); + + const redirRes = Response.redirect("https://example.com/login", 302); + console.log("redir_status: " + redirRes.status); + console.log("redir_loc: " + redirRes.headers.get("location")); + + const staticJsonRes = Response.json({ score: 42 }); + const sj = await staticJsonRes.json<{ score: number }>(); + console.log("static_json: " + sj.score); +} +testResponse(); diff --git a/internal/compiler/testdata/corpus/api/urlpattern.ts b/internal/compiler/testdata/corpus/api/urlpattern.ts new file mode 100644 index 0000000..41a5c01 --- /dev/null +++ b/internal/compiler/testdata/corpus/api/urlpattern.ts @@ -0,0 +1,83 @@ +// ScriptGo Corpus: WHATWG URLPattern Standard API +// Consolidated test suite with inline assertions. + +import { URLPattern } from "node:url"; + +// --- 1. Basic Pathname Pattern with Named Parameter --- +// @api: urlpattern.constructor +// @api: urlpattern.pathname +// @api: urlpattern.test +// @api: urlpattern.exec +// @native.expected: /books/:id +// @native.expected: true +// @native.expected: false +// @native.expected: 123 +const p1 = new URLPattern({ pathname: "/books/:id" }); +console.log(p1.pathname); +console.log(p1.test("https://example.com/books/123")); +console.log(p1.test("https://example.com/authors/123")); + +const res1 = p1.exec("https://example.com/books/123"); +if (res1 !== null) { + console.log(res1.pathname.groups["id"]); +} + +// --- 2. Multiple Named Parameters in Pathname --- +// @native.expected: v2 +// @native.expected: 42 +const p2 = new URLPattern({ pathname: "/api/:version/users/:userId" }); +const res2 = p2.exec("https://api.example.com/api/v2/users/42"); +if (res2 !== null) { + console.log(res2.pathname.groups["version"]); + console.log(res2.pathname.groups["userId"]); +} + +// --- 3. Wildcard Pattern --- +// @native.expected: css/theme.css +const p3 = new URLPattern({ pathname: "/static/*" }); +const res3 = p3.exec("/static/css/theme.css"); +if (res3 !== null) { + console.log(res3.pathname.groups["0"]); +} + +// --- 4. Relative Pattern with baseURL --- +// @native.expected: https +// @native.expected: example.com +// @native.expected: 99 +const p4 = new URLPattern("/items/:itemId", "https://example.com"); +console.log(p4.protocol); +console.log(p4.hostname); +const res4 = p4.exec("https://example.com/items/99"); +if (res4 !== null) { + console.log(res4.pathname.groups["itemId"]); +} + +// --- 5. Search Query Parameter Matching --- +// @native.expected: scriptgo +const p5 = new URLPattern({ pathname: "/search", search: "q=:query" }); +const res5 = p5.exec("https://example.com/search?q=scriptgo"); +if (res5 !== null) { + console.log(res5.search.groups["query"]); +} + +// --- 6. Optional Parameters --- +// @native.expected: with_id: 101 +// @native.expected: no_id: true +const p6 = new URLPattern({ pathname: "/posts/:id?" }); +const res6a = p6.exec("/posts/101"); +if (res6a !== null) { + console.log("with_id: " + res6a.pathname.groups["id"]); +} +const res6b = p6.exec("/posts"); +if (res6b !== null) { + console.log("no_id: " + (res6b.pathname.groups["id"] === undefined)); +} + +// --- 7. Custom Regular Expression Constraint --- +// @native.expected: digits_only: true +// @native.expected: non_digits: false +// @native.expected: true +const p7 = new URLPattern({ pathname: "/orders/:orderId(\\d+)" }); +console.log("digits_only: " + p7.test("/orders/12345")); +console.log("non_digits: " + p7.test("/orders/abcde")); +console.log(p7.hasRegExpGroups); diff --git a/internal/runtime/native/regex/runtime.c b/internal/runtime/native/regex/runtime.c index 478ae1d..23247ab 100644 --- a/internal/runtime/native/regex/runtime.c +++ b/internal/runtime/native/regex/runtime.c @@ -10,63 +10,138 @@ int scriptgo_runtime_set_error(const char *message); int scriptgo_array_new(int64_t length, int64_t element_size, void **out_array); int scriptgo_array_set(void *array, double index, const void *element); -static char *normalize_pattern(const char *pattern) { - if (pattern == NULL) return NULL; +extern const char scriptgo_undefined_sentinel; + +#define MAX_REGEX_GROUPS 64 + +typedef struct { + char *pattern; + int posix_group_count; + int js_group_count; + int is_capturing[MAX_REGEX_GROUPS]; +} normalized_regex_t; + +static int normalize_pattern_full(const char *pattern, normalized_regex_t *out) { + if (pattern == NULL || out == NULL) return -1; size_t len = strlen(pattern); char *buf = malloc(len * 16 + 1); - if (buf == NULL) return NULL; - size_t out = 0; - for (size_t i = 0; i < len; i++) { + if (buf == NULL) return -1; + + out->posix_group_count = 0; + out->js_group_count = 0; + for (int g = 0; g < MAX_REGEX_GROUPS; g++) { + out->is_capturing[g] = 0; + } + + size_t out_idx = 0; + size_t i = 0; + int in_bracket = 0; + + while (i < len) { if (pattern[i] == '\\' && i + 1 < len) { char next = pattern[i + 1]; - if (next == 'd') { - const char *rep = "[0-9]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; - i++; - continue; - } else if (next == 'D') { - const char *rep = "[^0-9]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; - i++; - continue; - } else if (next == 'w') { - const char *rep = "[a-zA-Z0-9_]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; - i++; - continue; - } else if (next == 'W') { - const char *rep = "[^a-zA-Z0-9_]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; - i++; - continue; - } else if (next == 's') { - const char *rep = "[ \t\r\n\f\v]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; - i++; + if (!in_bracket) { + if (next == 'd') { + const char *rep = "[0-9]"; + size_t rlen = 5; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } else if (next == 'D') { + const char *rep = "[^0-9]"; + size_t rlen = 6; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } else if (next == 'w') { + const char *rep = "[a-zA-Z0-9_]"; + size_t rlen = 12; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } else if (next == 'W') { + const char *rep = "[^a-zA-Z0-9_]"; + size_t rlen = 13; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } else if (next == 's') { + const char *rep = "[ \t\r\n\f\v]"; + size_t rlen = 8; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } else if (next == 'S') { + const char *rep = "[^ \t\r\n\f\v]"; + size_t rlen = 9; + memcpy(buf + out_idx, rep, rlen); + out_idx += rlen; + i += 2; + continue; + } + } + buf[out_idx++] = '\\'; + buf[out_idx++] = next; + i += 2; + continue; + } + + if (pattern[i] == '[') { + in_bracket = 1; + buf[out_idx++] = pattern[i++]; + if (i < len && pattern[i] == '^') { + buf[out_idx++] = pattern[i++]; + } + if (i < len && pattern[i] == ']') { + buf[out_idx++] = pattern[i++]; + } + continue; + } + + if (pattern[i] == ']' && in_bracket) { + in_bracket = 0; + buf[out_idx++] = pattern[i++]; + continue; + } + + if (!in_bracket && pattern[i] == '(') { + if (i + 2 < len && pattern[i + 1] == '?' && pattern[i + 2] == ':') { + out->posix_group_count++; + if (out->posix_group_count < MAX_REGEX_GROUPS) { + out->is_capturing[out->posix_group_count] = 0; + } + buf[out_idx++] = '('; + i += 3; continue; - } else if (next == 'S') { - const char *rep = "[^ \t\r\n\f\v]"; - size_t rlen = strlen(rep); - memcpy(buf + out, rep, rlen); - out += rlen; + } else { + out->posix_group_count++; + if (out->posix_group_count < MAX_REGEX_GROUPS) { + out->is_capturing[out->posix_group_count] = 1; + } + out->js_group_count++; + buf[out_idx++] = '('; i++; continue; } } - buf[out++] = pattern[i]; + + buf[out_idx++] = pattern[i++]; } - buf[out] = '\0'; - return buf; + + buf[out_idx] = '\0'; + out->pattern = buf; + return 0; +} + +static char *normalize_pattern(const char *pattern) { + normalized_regex_t norm; + if (normalize_pattern_full(pattern, &norm) != 0) return NULL; + return norm.pattern; } int scriptgo_regex_test(const char *pattern, const char *flags, const char *str, double *out_bool) { @@ -113,15 +188,27 @@ int scriptgo_regex_exec_stateful(const char *pattern, const char *flags, const c if (strchr(flags, 'm') != NULL) cflags |= REG_NEWLINE; } regex_t re; - char *norm = normalize_pattern(pattern); - if (regcomp(&re, norm ? norm : pattern, cflags) != 0) { - if (norm) free(norm); + normalized_regex_t norm; + if (normalize_pattern_full(pattern, &norm) != 0) { + return scriptgo_runtime_set_error("regex normalization failed"); + } + if (regcomp(&re, norm.pattern, cflags) != 0) { + free(norm.pattern); return scriptgo_runtime_set_error("invalid regular expression"); } - if (norm) free(norm); - regmatch_t pmatch[16]; - int status = regexec(&re, str + last_idx, 16, pmatch, 0); + free(norm.pattern); + + size_t nmatch = re.re_nsub + 1; + if (nmatch < 1) nmatch = 1; + regmatch_t *pmatch = malloc(nmatch * sizeof(regmatch_t)); + if (pmatch == NULL) { + regfree(&re); + return scriptgo_runtime_set_error("regex match allocation failed"); + } + + int status = regexec(&re, str + last_idx, nmatch, pmatch, 0); if (status != 0) { + free(pmatch); regfree(&re); if (is_global && inout_last_index != NULL) *inout_last_index = 0.0; *out_array = NULL; @@ -130,22 +217,45 @@ int scriptgo_regex_exec_stateful(const char *pattern, const char *flags, const c if (is_global && inout_last_index != NULL) { *inout_last_index = (double)(last_idx + pmatch[0].rm_eo); } - int count = 0; - for (int i = 0; i < 16; i++) { - if (pmatch[i].rm_so != -1) count++; - } + + int result_count = 1 + norm.js_group_count; regfree(&re); - int err = scriptgo_array_new(count, sizeof(const char*), out_array); - if (err != 0) return err; - for (int i = 0; i < count; i++) { - int len = pmatch[i].rm_eo - pmatch[i].rm_so; - char *sub = malloc(len + 1); - if (sub != NULL) { - memcpy(sub, str + last_idx + pmatch[i].rm_so, len); - sub[len] = '\0'; - scriptgo_array_set(*out_array, (double)i, &sub); + + int err = scriptgo_array_new(result_count, sizeof(const char *), out_array); + if (err != 0) { + free(pmatch); + return err; + } + + int len0 = pmatch[0].rm_eo - pmatch[0].rm_so; + char *sub0 = malloc(len0 + 1); + if (sub0 != NULL) { + memcpy(sub0, str + last_idx + pmatch[0].rm_so, len0); + sub0[len0] = '\0'; + scriptgo_array_set(*out_array, 0.0, &sub0); + } + + int js_idx = 1; + for (int p = 1; p <= norm.posix_group_count && (size_t)p < nmatch; p++) { + if (!norm.is_capturing[p]) { + continue; } + if (pmatch[p].rm_so == -1) { + const char *undef = &scriptgo_undefined_sentinel; + scriptgo_array_set(*out_array, (double)js_idx, &undef); + } else { + int len = pmatch[p].rm_eo - pmatch[p].rm_so; + char *sub = malloc(len + 1); + if (sub != NULL) { + memcpy(sub, str + last_idx + pmatch[p].rm_so, len); + sub[len] = '\0'; + scriptgo_array_set(*out_array, (double)js_idx, &sub); + } + } + js_idx++; } + + free(pmatch); return 0; } diff --git a/internal/typescriptgo/module_graph.go b/internal/typescriptgo/module_graph.go index 12bed13..4466a65 100644 --- a/internal/typescriptgo/module_graph.go +++ b/internal/typescriptgo/module_graph.go @@ -14,6 +14,7 @@ var webGlobalModules = map[string]string{ "FormData": "formdata", "URLSearchParams": "url", "URL": "url", + "URLPattern": "urlpattern", "Headers": "http", "Response": "http", "Request": "http", diff --git a/internal/typescriptgo/stdlib/buffer.ts b/internal/typescriptgo/stdlib/buffer.ts index 021c8de..bf3fd58 100644 --- a/internal/typescriptgo/stdlib/buffer.ts +++ b/internal/typescriptgo/stdlib/buffer.ts @@ -122,7 +122,7 @@ type BlobPart = string | Buffer | Uint8Array | ArrayBuffer | Blob; export class Blob { size: number = 0; type: string = ""; - private _bytes: Buffer = Buffer.alloc(0); + _bytes: Buffer = Buffer.alloc(0); constructor(sources: BlobPart[] = [], options?: { type?: string }) { const parts: Buffer[] = []; diff --git a/internal/typescriptgo/stdlib/globals.d.ts b/internal/typescriptgo/stdlib/globals.d.ts index ccc66f7..b1dcfec 100644 --- a/internal/typescriptgo/stdlib/globals.d.ts +++ b/internal/typescriptgo/stdlib/globals.d.ts @@ -832,6 +832,61 @@ interface URLConstructor { declare var URL: URLConstructor; +interface URLPatternComponentResult { + input: string; + groups: Record; +} + +interface URLPatternResult { + inputs: (string | URLPatternInput)[]; + protocol: URLPatternComponentResult; + username: URLPatternComponentResult; + password: URLPatternComponentResult; + hostname: URLPatternComponentResult; + port: URLPatternComponentResult; + pathname: URLPatternComponentResult; + search: URLPatternComponentResult; + hash: URLPatternComponentResult; +} + +interface URLPatternInput { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} + +interface URLPatternOptions { + ignoreCase?: boolean; +} + +interface URLPattern { + readonly protocol: string; + readonly username: string; + readonly password: string; + readonly hostname: string; + readonly port: string; + readonly pathname: string; + readonly search: string; + readonly hash: string; + readonly hasRegExpGroups: boolean; + test(input?: string | URLPatternInput, baseURL?: string): boolean; + exec(input?: string | URLPatternInput, baseURL?: string): URLPatternResult | null; +} + +interface URLPatternConstructor { + new(input?: string | URLPatternInput, baseURL?: string, options?: URLPatternOptions): URLPattern; + new(input?: string | URLPatternInput, options?: URLPatternOptions): URLPattern; + readonly prototype: URLPattern; +} + +declare var URLPattern: URLPatternConstructor; + interface HeadersIterator extends IterableIterator { next(): IteratorResult; [Symbol.iterator](): HeadersIterator; @@ -885,10 +940,12 @@ interface FormDataConstructor { declare var FormData: FormDataConstructor; +type BodyInit = string | Blob | FormData | URLSearchParams | ArrayBuffer | Uint8Array; + interface RequestInit { method?: string; - headers?: Headers | Record | [string, string][]; - body?: string | null; + headers?: Headers | Record | [string, string][] | unknown; + body?: BodyInit | null; } interface Request { @@ -896,6 +953,13 @@ interface Request { readonly method: string; readonly headers: Headers; readonly body: string | null; + text(): Promise; + json(): Promise; + arrayBuffer(): Promise; + blob(): Promise; + bytes(): Promise; + formData(): Promise; + clone(): Request; } interface RequestConstructor { @@ -908,7 +972,7 @@ declare var Request: RequestConstructor; interface ResponseInit { status?: number; statusText?: string; - headers?: Headers | Record | [string, string][]; + headers?: Headers | Record | [string, string][] | unknown; } interface Response { @@ -921,11 +985,14 @@ interface Response { text(): Promise; json(): Promise; arrayBuffer(): Promise; + blob(): Promise; + bytes(): Promise; formData(): Promise; + clone(): Response; } interface ResponseConstructor { - new(body?: string | null, init?: ResponseInit): Response; + new(body?: BodyInit | null, init?: ResponseInit): Response; readonly prototype: Response; json(data: unknown, init?: ResponseInit): Response; error(): Response; diff --git a/internal/typescriptgo/stdlib/http.ts b/internal/typescriptgo/stdlib/http.ts index 341ba67..fc9d240 100644 --- a/internal/typescriptgo/stdlib/http.ts +++ b/internal/typescriptgo/stdlib/http.ts @@ -5,9 +5,9 @@ import { ServerOptions as NetServerOptions, connect as netConnect } from "node:net"; -import { URL } from "node:url"; +import { URL, URLSearchParams } from "node:url"; import { FormData } from "node:formdata"; -import { File } from "node:buffer"; +import { Blob, File } from "node:buffer"; export class HeadersIterator { private _values: T[]; @@ -148,10 +148,149 @@ export class Headers { } } +function encodeFormDataBody(formData: FormData): { body: string; contentType: string } { + const boundary = "----ScriptGoFormBoundary" + String(Date.now()) + String(Math.floor(Math.random() * 1000000)); + const entries = formData.entries(); + let body = ""; + for (let it = entries.next(); !it.done; it = entries.next()) { + const entry = it.value; + if (!entry) continue; + const name = entry[0]; + const val = entry[1]; + body += "--" + boundary + "\r\n"; + if (val instanceof File) { + const file = val as File; + const filename = file.name && file.name.length > 0 ? file.name : "blob"; + const fileType = file.type && file.type.length > 0 ? file.type : "application/octet-stream"; + body += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n"; + body += "Content-Type: " + fileType + "\r\n\r\n"; + body += file._bytes.toString() + "\r\n"; + } else if (val instanceof Blob) { + const blob = val as Blob; + const fileType = blob.type && blob.type.length > 0 ? blob.type : "application/octet-stream"; + body += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"blob\"\r\n"; + body += "Content-Type: " + fileType + "\r\n\r\n"; + body += blob._bytes.toString() + "\r\n"; + } else { + body += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"; + body += String(val) + "\r\n"; + } + } + body += "--" + boundary + "--\r\n"; + return { + body: body, + contentType: "multipart/form-data; boundary=" + boundary + }; +} + +function parseFormDataFromBody(rawBody: string, headers: Headers): FormData { + const fd = new FormData(); + const ct = headers.get("content-type") || ""; + if (ct.includes("multipart/form-data")) { + let boundary = ""; + const bIdx = ct.indexOf("boundary="); + if (bIdx !== -1) { + boundary = ct.substring(bIdx + 9).trim(); + if (boundary.startsWith('"') && boundary.endsWith('"')) { + boundary = boundary.substring(1, boundary.length - 1); + } + } + if (boundary.length > 0) { + const delimiter = "--" + boundary; + const parts = rawBody.split(delimiter); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.length === 0 || part === "--" || part === "--\r\n" || part.startsWith("--")) { + continue; + } + const headerEnd = part.indexOf("\r\n\r\n"); + const lfHeaderEnd = part.indexOf("\n\n"); + let headerBlock = ""; + let bodyBlock = ""; + if (headerEnd !== -1) { + headerBlock = part.substring(0, headerEnd); + bodyBlock = part.substring(headerEnd + 4); + if (bodyBlock.endsWith("\r\n")) { + bodyBlock = bodyBlock.substring(0, bodyBlock.length - 2); + } + } else if (lfHeaderEnd !== -1) { + headerBlock = part.substring(0, lfHeaderEnd); + bodyBlock = part.substring(lfHeaderEnd + 2); + if (bodyBlock.endsWith("\n")) { + bodyBlock = bodyBlock.substring(0, bodyBlock.length - 1); + } + } else { + continue; + } + let name = ""; + let filename = ""; + let partContentType = "text/plain"; + const lines = headerBlock.split("\n"); + for (let j = 0; j < lines.length; j++) { + const line = lines[j].trim(); + const lower = line.toLowerCase(); + if (lower.startsWith("content-disposition:")) { + const nameMatch = line.indexOf("name=\""); + if (nameMatch !== -1) { + const endQuote = line.indexOf("\"", nameMatch + 6); + if (endQuote !== -1) { + name = line.substring(nameMatch + 6, endQuote); + } + } + const fnMatch = line.indexOf("filename=\""); + if (fnMatch !== -1) { + const fnEnd = line.indexOf("\"", fnMatch + 10); + if (fnEnd !== -1) { + filename = line.substring(fnMatch + 10, fnEnd); + } + } + } else if (lower.startsWith("content-type:")) { + partContentType = line.substring(13).trim(); + } + } + if (name.length > 0) { + if (filename.length > 0) { + fd.append(name, new File([bodyBlock], filename, { type: partContentType })); + } else { + fd.append(name, bodyBlock); + } + } + } + } + return fd; + } + + if (rawBody.length > 0) { + const pairs = rawBody.split("&"); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i]; + if (pair.length === 0) continue; + const eq = pair.indexOf("="); + if (eq !== -1) { + const rawKey = pair.substring(0, eq).replaceAll("+", " "); + const rawVal = pair.substring(eq + 1).replaceAll("+", " "); + try { + fd.append(decodeURIComponent(rawKey), decodeURIComponent(rawVal)); + } catch (e) { + fd.append(rawKey, rawVal); + } + } else { + const rawKey = pair.replaceAll("+", " "); + try { + fd.append(decodeURIComponent(rawKey), ""); + } catch (e) { + fd.append(rawKey, ""); + } + } + } + } + return fd; +} + export interface RequestInit { method?: string; headers?: unknown; - body?: string | null; + body?: unknown; } const defaultRequestInit: RequestInit = { method: "", headers: null, body: "" }; @@ -164,15 +303,16 @@ export class Request { constructor(input: unknown, init: RequestInit = defaultRequestInit) { if (typeof input === "string") { - this.url = input; + this.url = input as string; this.method = "GET"; this.headers = new Headers(); this.body = ""; } else if (input instanceof Request) { - this.url = input.url; - this.method = input.method; - this.headers = new Headers(input.headers); - this.body = input.body; + const other = input as Request; + this.url = other.url; + this.method = other.method; + this.headers = new Headers(other.headers); + this.body = other.body; } else { this.url = ""; this.method = "GET"; @@ -183,12 +323,76 @@ export class Request { if (init.method !== undefined && init.method.length > 0) { this.method = init.method.toUpperCase(); } - if (init.headers instanceof Headers) { - this.headers = init.headers as Headers; + if (init.headers !== undefined && init.headers !== null) { + if (init.headers instanceof Headers) { + this.headers = init.headers as Headers; + } else { + this.headers = new Headers(init.headers as any); + } } - if (init.body !== undefined && init.body !== null && init.body.length > 0) { - this.body = init.body; + if (init.body !== undefined && init.body !== null) { + const b = init.body as any; + if (typeof b === "string") { + this.body = b as string; + } else if (b instanceof FormData) { + const enc = encodeFormDataBody(b as FormData); + this.body = enc.body; + if (!this.headers.has("content-type")) { + this.headers.set("content-type", enc.contentType); + } + } else if (b instanceof URLSearchParams) { + this.body = (b as URLSearchParams).toString(); + if (!this.headers.has("content-type")) { + this.headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8"); + } + } else if (b instanceof Blob) { + this.body = (b as Blob)._bytes.toString(); + if (!this.headers.has("content-type") && (b as Blob).type.length > 0) { + this.headers.set("content-type", (b as Blob).type); + } + } else { + this.body = String(b); + } + } + } + + async text(): Promise { + return this.body; + } + + async json(): Promise { + return JSON.parse(this.body) as T; + } + + async arrayBuffer(): Promise { + const buf = new Uint8Array(this.body.length); + for (let i = 0; i < this.body.length; i++) { + buf[i] = this.body.charCodeAt(i); } + return buf.buffer as ArrayBuffer; + } + + async blob(): Promise { + const ct = this.headers.get("content-type") || ""; + return new Blob([this.body], { type: ct }); + } + + async bytes(): Promise { + const ab = await this.arrayBuffer(); + return new Uint8Array(ab); + } + + async formData(): Promise { + return parseFormDataFromBody(this.body, this.headers); + } + + clone(): Request { + const init: RequestInit = { + method: this.method, + headers: new Headers(this.headers), + body: this.body + }; + return new Request(this.url, init); } } @@ -209,21 +413,51 @@ export class Response { body: unknown = null; _body: string = ""; - constructor(body: string = "", init: ResponseInit = defaultResponseInit) { - this._body = body; + constructor(body: unknown = "", init: ResponseInit = defaultResponseInit) { this.body = null; let s = 200; let st = "OK"; let h = new Headers(); - if (init.status !== undefined && init.status > 0) { + if (init.status !== undefined && init.status !== null) { s = init.status; } - if (init.statusText !== undefined && init.statusText.length > 0) { + if (init.statusText !== undefined && init.statusText !== null) { st = init.statusText; } if (init.headers !== undefined && init.headers !== null) { - h = new Headers(init.headers as any); + if (init.headers instanceof Headers) { + h = new Headers(init.headers as Headers); + } else { + h = new Headers(init.headers as any); + } } + + let bodyStr = ""; + if (body !== undefined && body !== null) { + const b = body as any; + if (typeof b === "string") { + bodyStr = b as string; + } else if (b instanceof FormData) { + const enc = encodeFormDataBody(b as FormData); + bodyStr = enc.body; + if (!h.has("content-type")) { + h.set("content-type", enc.contentType); + } + } else if (b instanceof URLSearchParams) { + bodyStr = (b as URLSearchParams).toString(); + if (!h.has("content-type")) { + h.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8"); + } + } else if (b instanceof Blob) { + bodyStr = (b as Blob)._bytes.toString(); + if (!h.has("content-type") && (b as Blob).type.length > 0) { + h.set("content-type", (b as Blob).type); + } + } else { + bodyStr = String(b); + } + } + this._body = bodyStr; this.status = s; this.statusText = st; this.ok = (s >= 200 && s < 300); @@ -247,114 +481,37 @@ export class Response { return buf.buffer as ArrayBuffer; } - async formData(): Promise { - const fd = new FormData(); + async blob(): Promise { const ct = this.headers.get("content-type") || ""; - if (ct.includes("multipart/form-data")) { - let boundary = ""; - const bIdx = ct.indexOf("boundary="); - if (bIdx !== -1) { - boundary = ct.substring(bIdx + 9).trim(); - if (boundary.startsWith('"') && boundary.endsWith('"')) { - boundary = boundary.substring(1, boundary.length - 1); - } - } - if (boundary.length > 0) { - const delimiter = "--" + boundary; - const parts = this._body.split(delimiter); - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part.length === 0 || part === "--" || part === "--\r\n" || part.startsWith("--")) { - continue; - } - const headerEnd = part.indexOf("\r\n\r\n"); - const lfHeaderEnd = part.indexOf("\n\n"); - let headerBlock = ""; - let bodyBlock = ""; - if (headerEnd !== -1) { - headerBlock = part.substring(0, headerEnd); - bodyBlock = part.substring(headerEnd + 4); - if (bodyBlock.endsWith("\r\n")) { - bodyBlock = bodyBlock.substring(0, bodyBlock.length - 2); - } - } else if (lfHeaderEnd !== -1) { - headerBlock = part.substring(0, lfHeaderEnd); - bodyBlock = part.substring(lfHeaderEnd + 2); - if (bodyBlock.endsWith("\n")) { - bodyBlock = bodyBlock.substring(0, bodyBlock.length - 1); - } - } else { - continue; - } - let name = ""; - let filename = ""; - let partContentType = "text/plain"; - const lines = headerBlock.split("\n"); - for (let j = 0; j < lines.length; j++) { - const line = lines[j].trim(); - const lower = line.toLowerCase(); - if (lower.startsWith("content-disposition:")) { - const nameMatch = line.indexOf("name=\""); - if (nameMatch !== -1) { - const endQuote = line.indexOf("\"", nameMatch + 6); - if (endQuote !== -1) { - name = line.substring(nameMatch + 6, endQuote); - } - } - const fnMatch = line.indexOf("filename=\""); - if (fnMatch !== -1) { - const fnEnd = line.indexOf("\"", fnMatch + 10); - if (fnEnd !== -1) { - filename = line.substring(fnMatch + 10, fnEnd); - } - } - } else if (lower.startsWith("content-type:")) { - partContentType = line.substring(13).trim(); - } - } - if (name.length > 0) { - if (filename.length > 0) { - fd.append(name, new File([bodyBlock], filename, { type: partContentType })); - } else { - fd.append(name, bodyBlock); - } - } - } - } - return fd; - } - - if (this._body.length > 0) { - const pairs = this._body.split("&"); - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i]; - if (pair.length === 0) continue; - const eq = pair.indexOf("="); - if (eq !== -1) { - const rawKey = pair.substring(0, eq).replaceAll("+", " "); - const rawVal = pair.substring(eq + 1).replaceAll("+", " "); - try { - fd.append(decodeURIComponent(rawKey), decodeURIComponent(rawVal)); - } catch (e) { - fd.append(rawKey, rawVal); - } - } else { - const rawKey = pair.replaceAll("+", " "); - try { - fd.append(decodeURIComponent(rawKey), ""); - } catch (e) { - fd.append(rawKey, ""); - } - } - } - } - return fd; + return new Blob([this._body], { type: ct }); } - static json(data: string, init: ResponseInit = defaultResponseInit): Response { + async bytes(): Promise { + const ab = await this.arrayBuffer(); + return new Uint8Array(ab); + } + + async formData(): Promise { + return parseFormDataFromBody(this._body, this.headers); + } + + clone(): Response { + const init: ResponseInit = { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers) + }; + const cloned = new Response(this._body, init); + cloned.url = this.url; + return cloned; + } + + static json(data: unknown, init: ResponseInit = defaultResponseInit): Response { let headers = new Headers(); if (init.headers instanceof Headers) { headers = new Headers(init.headers as Headers); + } else if (init.headers !== null && init.headers !== undefined) { + headers = new Headers(init.headers as any); } if (!headers.has("content-type")) { headers.set("content-type", "application/json"); @@ -372,7 +529,8 @@ export class Response { statusText: st, headers: headers }; - return new Response(data, respInit); + const bodyStr = typeof data === "string" ? data : JSON.stringify(data); + return new Response(bodyStr, respInit); } static error(): Response { @@ -415,45 +573,29 @@ declare namespace __scriptgo { } export async function fetch(input: unknown, init: RequestInit = defaultRequestInit): Promise { - let url = ""; - let method = "GET"; - let body = ""; - let headers: Headers = new Headers(); - if (typeof input === "string") { - url = input; - } else if (input instanceof Request) { - url = input.url; - method = input.method; - body = input.body; - headers = input.headers; - } - if (init.method !== undefined && init.method.length > 0) { - method = init.method; - } - if (init.body !== undefined && init.body !== null && init.body.length > 0) { - body = init.body; - } - if (init.headers instanceof Headers) { - headers = init.headers as Headers; - } + const req = (input instanceof Request && init === defaultRequestInit) + ? (input as Request) + : new Request(input, init); + const flatHeaders: string[] = []; - for (let i = 0; i < headers._keys.length; i++) { - flatHeaders.push(headers._keys[i]); - flatHeaders.push(headers._values[i]); + for (let i = 0; i < req.headers._keys.length; i++) { + flatHeaders.push(req.headers._keys[i]); + flatHeaders.push(req.headers._values[i]); } - const raw = __scriptgo.fetchSync(url, method, flatHeaders, body); + const raw = __scriptgo.fetchSync(req.url, req.method, flatHeaders, req.body); const respHeaders = new Headers(); for (let i = 0; i < raw.headers.length; i += 2) { if (i + 1 < raw.headers.length) { respHeaders.append(raw.headers[i], raw.headers[i + 1]); } } - const respInit: ResponseInit = { + const resp = new Response(raw.body, { status: raw.status, statusText: raw.statusText, headers: respHeaders - }; - return new Response(raw.body, respInit); + }); + resp.url = req.url; + return resp; } export const METHODS: string[] = [ diff --git a/internal/typescriptgo/stdlib/module.ts b/internal/typescriptgo/stdlib/module.ts index d5fff2e..4752af4 100644 --- a/internal/typescriptgo/stdlib/module.ts +++ b/internal/typescriptgo/stdlib/module.ts @@ -28,6 +28,7 @@ export const builtinModules: string[] = [ "tls", "tty", "url", + "urlpattern", "util", "zlib" ]; diff --git a/internal/typescriptgo/stdlib/url.ts b/internal/typescriptgo/stdlib/url.ts index 4834b6e..8365015 100644 --- a/internal/typescriptgo/stdlib/url.ts +++ b/internal/typescriptgo/stdlib/url.ts @@ -591,3 +591,12 @@ export function urlToHttpOptions(url: URL): HttpOptionsResult { auth: url.username.length > 0 ? (url.password.length > 0 ? url.username + ":" + url.password : url.username) : null }; } + +export { + URLPattern, + URLPatternInput, + URLPatternResult, + URLPatternComponentResult, + URLPatternOptions +} from "node:urlpattern"; + diff --git a/internal/typescriptgo/stdlib/urlpattern.ts b/internal/typescriptgo/stdlib/urlpattern.ts new file mode 100644 index 0000000..c4e5c58 --- /dev/null +++ b/internal/typescriptgo/stdlib/urlpattern.ts @@ -0,0 +1,594 @@ +// WHATWG URLPattern Standard API Implementation for ScriptGo + +export interface URLPatternComponentResult { + input: string; + groups: Record; +} + +export interface URLPatternResult { + inputs: (string | URLPatternInput)[]; + protocol: URLPatternComponentResult; + username: URLPatternComponentResult; + password: URLPatternComponentResult; + hostname: URLPatternComponentResult; + port: URLPatternComponentResult; + pathname: URLPatternComponentResult; + search: URLPatternComponentResult; + hash: URLPatternComponentResult; +} + +export interface URLPatternInput { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} + +export interface URLPatternOptions { + ignoreCase?: boolean; +} + +interface ParsedURLParts { + protocol: string; + username: string; + password: string; + hostname: string; + port: string; + pathname: string; + search: string; + hash: string; +} + +function parseURLParts(urlStr: string): ParsedURLParts { + let protocol = ""; + let username = ""; + let password = ""; + let hostname = ""; + let port = ""; + let pathname = ""; + let search = ""; + let hash = ""; + + let rest = urlStr; + const protoIdx = rest.indexOf("://"); + if (protoIdx !== -1) { + protocol = rest.substring(0, protoIdx); + rest = rest.substring(protoIdx + 3); + + let pathStart = rest.indexOf("/"); + const qStart = rest.indexOf("?"); + const hStart = rest.indexOf("#"); + if (pathStart === -1 || (qStart !== -1 && qStart < pathStart)) { + pathStart = qStart; + } + if (pathStart === -1 || (hStart !== -1 && hStart < pathStart)) { + pathStart = hStart; + } + + let authority = rest; + if (pathStart !== -1) { + authority = rest.substring(0, pathStart); + rest = rest.substring(pathStart); + } else { + authority = rest; + rest = "/"; + } + + const atIdx = authority.indexOf("@"); + if (atIdx !== -1) { + const userinfo = authority.substring(0, atIdx); + authority = authority.substring(atIdx + 1); + const colonIdx = userinfo.indexOf(":"); + if (colonIdx !== -1) { + username = userinfo.substring(0, colonIdx); + password = userinfo.substring(colonIdx + 1); + } else { + username = userinfo; + } + } + + const colonIdx = authority.lastIndexOf(":"); + if (colonIdx !== -1) { + hostname = authority.substring(0, colonIdx); + port = authority.substring(colonIdx + 1); + } else { + hostname = authority; + } + } + + const hashIdx = rest.indexOf("#"); + if (hashIdx !== -1) { + hash = rest.substring(hashIdx + 1); + rest = rest.substring(0, hashIdx); + } + + const searchIdx = rest.indexOf("?"); + if (searchIdx !== -1) { + search = rest.substring(searchIdx + 1); + rest = rest.substring(0, searchIdx); + } + + pathname = rest; + if (pathname.length === 0) { + pathname = "/"; + } + + return { protocol, username, password, hostname, port, pathname, search, hash }; +} + +function isIdentChar(ch: string): boolean { + const code = ch.charCodeAt(0); + return (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95; +} + +function isRegexSpecial(ch: string): boolean { + return ch === "." || ch === "+" || ch === "?" || ch === "^" || ch === "$" || + ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "{" || + ch === "}" || ch === "|"; +} + +class ComponentPattern { + raw: string; + regex: RegExp; + groupNames: string[]; + hasCustomRegex: boolean; + isWildcard: boolean; + + constructor(rawPattern: string, isPath: boolean, ignoreCase: boolean = false) { + this.raw = rawPattern; + this.groupNames = []; + this.hasCustomRegex = false; + this.isWildcard = false; + + if (rawPattern === "*") { + this.isWildcard = true; + this.groupNames.push("0"); + this.regex = RegExp("^(.*)$", ignoreCase ? "i" : ""); + return; + } + + if (rawPattern === "") { + this.regex = RegExp("^$", ignoreCase ? "i" : ""); + return; + } + + let regexStr = "^"; + let anonIdx = 0; + let i = 0; + const n = rawPattern.length; + + while (i < n) { + const ch = rawPattern.charAt(i); + if (ch === "\\") { + if (i + 1 < n) { + regexStr += "\\" + rawPattern.charAt(i + 1); + i += 2; + } else { + regexStr += "\\\\"; + i++; + } + continue; + } + + if (ch === "*") { + regexStr += "(.*)"; + this.groupNames.push(String(anonIdx)); + anonIdx = anonIdx + 1; + i++; + continue; + } + + if (ch === "{") { + const closeIdx = rawPattern.indexOf("}", i + 1); + if (closeIdx !== -1) { + const inner = rawPattern.substring(i + 1, closeIdx); + const isOptional = (closeIdx + 1 < n && rawPattern.charAt(closeIdx + 1) === "?"); + let subRegex = ""; + let j = 0; + while (j < inner.length) { + const subCh = inner.charAt(j); + if (subCh === ":") { + let nStart = j + 1; + let nEnd = nStart; + while (nEnd < inner.length && isIdentChar(inner.charAt(nEnd))) { + nEnd++; + } + const pName = inner.substring(nStart, nEnd); + j = nEnd; + const dReg = isPath ? "[^/]+" : "[^/?#&]+"; + subRegex += "(" + dReg + ")"; + this.groupNames.push(pName); + continue; + } + if (isRegexSpecial(subCh)) { + subRegex += "\\" + subCh; + } else { + subRegex += subCh; + } + j++; + } + if (isOptional) { + regexStr += "(?:" + subRegex + ")?"; + i = closeIdx + 2; + } else { + regexStr += "(?:" + subRegex + ")"; + i = closeIdx + 1; + } + continue; + } + } + + if (ch === ":") { + let nameStart = i + 1; + let nameEnd = nameStart; + while (nameEnd < n && isIdentChar(rawPattern.charAt(nameEnd))) { + nameEnd++; + } + const paramName = rawPattern.substring(nameStart, nameEnd); + i = nameEnd; + + let customReg = ""; + if (i < n && rawPattern.charAt(i) === "(") { + let parenDepth = 1; + let parenEnd = i + 1; + while (parenEnd < n && parenDepth > 0) { + if (rawPattern.charAt(parenEnd) === "\\") { + parenEnd += 2; + } else if (rawPattern.charAt(parenEnd) === "(") { + parenDepth++; + parenEnd++; + } else if (rawPattern.charAt(parenEnd) === ")") { + parenDepth--; + parenEnd++; + } else { + parenEnd++; + } + } + customReg = rawPattern.substring(i + 1, parenEnd - 1); + this.hasCustomRegex = true; + i = parenEnd; + } + + let modifier = ""; + if (i < n && (rawPattern.charAt(i) === "?" || rawPattern.charAt(i) === "+" || rawPattern.charAt(i) === "*")) { + modifier = rawPattern.charAt(i); + i++; + } + + const innerRegex = (customReg.length > 0) + ? customReg + : (isPath ? "[^/]+" : "[^/?#&]+"); + + if (modifier === "?") { + if (regexStr.endsWith("/")) { + regexStr = regexStr.substring(0, regexStr.length - 1) + "(?:/(" + innerRegex + "))?"; + } else { + regexStr += "(?:(" + innerRegex + "))?"; + } + } else if (modifier === "+") { + regexStr += "((?:" + innerRegex + ")+)"; + } else if (modifier === "*") { + regexStr += "((?:" + innerRegex + ")*)"; + } else { + regexStr += "(" + innerRegex + ")"; + } + + this.groupNames.push(paramName); + continue; + } + + if (isRegexSpecial(ch)) { + regexStr += "\\" + ch; + } else { + regexStr += ch; + } + i++; + } + + regexStr += "$"; + this.regex = RegExp(regexStr, ignoreCase ? "i" : ""); + } + + exec(input: string): { matched: boolean; groups: Record } { + if (this.isWildcard) { + const groups: Record = {}; + groups["0"] = input; + return { matched: true, groups: groups }; + } + if (!this.regex.test(input)) { + const emptyGroups: Record = {}; + return { matched: false, groups: emptyGroups }; + } + const m = this.regex.exec(input) as string[]; + const groups: Record = {}; + for (let i = 0; i < this.groupNames.length; i++) { + const gName = this.groupNames[i]; + let val: string | undefined = undefined; + if (m !== null && i + 1 < m.length && m[i + 1] !== undefined && m[i + 1] !== null) { + val = m[i + 1]; + } + groups[gName] = val; + } + return { matched: true, groups: groups }; + } +} + +export class URLPattern { + readonly protocol: string; + readonly username: string; + readonly password: string; + readonly hostname: string; + readonly port: string; + readonly pathname: string; + readonly search: string; + readonly hash: string; + readonly hasRegExpGroups: boolean; + + private _protocolPattern: ComponentPattern; + private _usernamePattern: ComponentPattern; + private _passwordPattern: ComponentPattern; + private _hostnamePattern: ComponentPattern; + private _portPattern: ComponentPattern; + private _pathnamePattern: ComponentPattern; + private _searchPattern: ComponentPattern; + private _hashPattern: ComponentPattern; + + constructor(input?: unknown, baseURLOrOptions?: unknown, optionsArg?: URLPatternOptions) { + let baseURL: string | undefined = undefined; + let options: URLPatternOptions | undefined = optionsArg; + + if (typeof baseURLOrOptions === "string") { + baseURL = baseURLOrOptions as string; + } else if (baseURLOrOptions && typeof baseURLOrOptions === "object") { + options = baseURLOrOptions as URLPatternOptions; + } + + const ignoreCase = (options !== undefined && options !== null && options.ignoreCase === true); + + let proto = "*"; + let user = "*"; + let pass = "*"; + let host = "*"; + let port = "*"; + let path = "*"; + let search = "*"; + let hash = "*"; + + if (typeof input === "string") { + const str = input as string; + let fullStr = str; + if (baseURL !== undefined && baseURL !== null && baseURL.length > 0) { + if (str.indexOf("://") === -1) { + const baseParts = parseURLParts(baseURL); + proto = baseParts.protocol.length > 0 ? baseParts.protocol : "*"; + user = baseParts.username.length > 0 ? baseParts.username : "*"; + pass = baseParts.password.length > 0 ? baseParts.password : "*"; + host = baseParts.hostname.length > 0 ? baseParts.hostname : "*"; + port = baseParts.port.length > 0 ? baseParts.port : "*"; + + const relParts = parseURLParts(str); + path = relParts.pathname; + search = str.indexOf("?") !== -1 ? relParts.search : ""; + hash = str.indexOf("#") !== -1 ? relParts.hash : ""; + fullStr = ""; + } + } + if (fullStr.length > 0) { + if (fullStr.indexOf("://") !== -1) { + const parts = parseURLParts(fullStr); + proto = parts.protocol; + user = parts.username; + pass = parts.password; + host = parts.hostname; + port = parts.port; + path = parts.pathname; + search = fullStr.indexOf("?") !== -1 ? parts.search : ""; + hash = fullStr.indexOf("#") !== -1 ? parts.hash : ""; + } else { + throw new TypeError("Failed to construct 'URLPattern': Invalid URL pattern."); + } + } + } else if (input && typeof input === "object") { + const obj = input as URLPatternInput; + if (obj.baseURL !== undefined && obj.baseURL !== null && obj.baseURL.length > 0) { + const baseParts = parseURLParts(obj.baseURL); + if (obj.protocol !== undefined) proto = obj.protocol; else if (baseParts.protocol.length > 0) proto = baseParts.protocol; + if (obj.username !== undefined) user = obj.username; else if (baseParts.username.length > 0) user = baseParts.username; + if (obj.password !== undefined) pass = obj.password; else if (baseParts.password.length > 0) pass = baseParts.password; + if (obj.hostname !== undefined) host = obj.hostname; else if (baseParts.hostname.length > 0) host = baseParts.hostname; + if (obj.port !== undefined) port = obj.port; else if (baseParts.port.length > 0) port = baseParts.port; + if (obj.pathname !== undefined) path = obj.pathname; else if (baseParts.pathname.length > 0) path = baseParts.pathname; + if (obj.search !== undefined) search = obj.search; else if (baseParts.search.length > 0) search = baseParts.search; + if (obj.hash !== undefined) hash = obj.hash; else if (baseParts.hash.length > 0) hash = baseParts.hash; + } else { + if (obj.protocol !== undefined) proto = obj.protocol; + if (obj.username !== undefined) user = obj.username; + if (obj.password !== undefined) pass = obj.password; + if (obj.hostname !== undefined) host = obj.hostname; + if (obj.port !== undefined) port = obj.port; + if (obj.pathname !== undefined) path = obj.pathname; + if (obj.search !== undefined) search = obj.search; + if (obj.hash !== undefined) hash = obj.hash; + } + } + + if (proto.endsWith(":")) { + proto = proto.substring(0, proto.length - 1); + } + if (search.startsWith("?")) { + search = search.substring(1); + } + if (hash.startsWith("#")) { + hash = hash.substring(1); + } + + this.protocol = proto; + this.username = user; + this.password = pass; + this.hostname = host; + this.port = port; + this.pathname = path; + this.search = search; + this.hash = hash; + + this._protocolPattern = new ComponentPattern(proto, false, ignoreCase); + this._usernamePattern = new ComponentPattern(user, false, ignoreCase); + this._passwordPattern = new ComponentPattern(pass, false, ignoreCase); + this._hostnamePattern = new ComponentPattern(host, false, ignoreCase); + this._portPattern = new ComponentPattern(port, false, ignoreCase); + this._pathnamePattern = new ComponentPattern(path, true, ignoreCase); + this._searchPattern = new ComponentPattern(search, false, ignoreCase); + this._hashPattern = new ComponentPattern(hash, false, ignoreCase); + + this.hasRegExpGroups = ( + this._protocolPattern.hasCustomRegex || + this._usernamePattern.hasCustomRegex || + this._passwordPattern.hasCustomRegex || + this._hostnamePattern.hasCustomRegex || + this._portPattern.hasCustomRegex || + this._pathnamePattern.hasCustomRegex || + this._searchPattern.hasCustomRegex || + this._hashPattern.hasCustomRegex + ); + } + + test(input?: unknown, baseURL?: string): boolean { + return this.exec(input, baseURL) !== null; + } + + exec(input?: unknown, baseURL?: string): URLPatternResult | null { + let candidate: ParsedURLParts = { + protocol: "", + username: "", + password: "", + hostname: "", + port: "", + pathname: "", + search: "", + hash: "" + }; + + const inputsList: (string | URLPatternInput)[] = []; + + if (typeof input === "string") { + const str = input as string; + if (baseURL !== undefined && baseURL !== null && baseURL.length > 0) { + inputsList.push(str); + inputsList.push(baseURL); + if (str.indexOf("://") === -1) { + const baseParts = parseURLParts(baseURL); + const relParts = parseURLParts(str); + candidate = { + protocol: baseParts.protocol, + username: baseParts.username, + password: baseParts.password, + hostname: baseParts.hostname, + port: baseParts.port, + pathname: relParts.pathname, + search: str.indexOf("?") !== -1 ? relParts.search : "", + hash: str.indexOf("#") !== -1 ? relParts.hash : "" + }; + } else { + candidate = parseURLParts(str); + } + } else { + inputsList.push(str); + candidate = parseURLParts(str); + if (str.indexOf("://") === -1) { + candidate.protocol = ""; + candidate.hostname = ""; + candidate.port = ""; + candidate.username = ""; + candidate.password = ""; + } + } + } else if (input && typeof input === "object") { + const obj = input as URLPatternInput; + inputsList.push(obj); + if (obj.baseURL !== undefined && obj.baseURL !== null && obj.baseURL.length > 0) { + const baseParts = parseURLParts(obj.baseURL); + candidate = { + protocol: obj.protocol !== undefined ? obj.protocol : baseParts.protocol, + username: obj.username !== undefined ? obj.username : baseParts.username, + password: obj.password !== undefined ? obj.password : baseParts.password, + hostname: obj.hostname !== undefined ? obj.hostname : baseParts.hostname, + port: obj.port !== undefined ? obj.port : baseParts.port, + pathname: obj.pathname !== undefined ? obj.pathname : baseParts.pathname, + search: obj.search !== undefined ? obj.search : baseParts.search, + hash: obj.hash !== undefined ? obj.hash : baseParts.hash + }; + } else { + candidate = { + protocol: obj.protocol !== undefined ? obj.protocol : "", + username: obj.username !== undefined ? obj.username : "", + password: obj.password !== undefined ? obj.password : "", + hostname: obj.hostname !== undefined ? obj.hostname : "", + port: obj.port !== undefined ? obj.port : "", + pathname: obj.pathname !== undefined ? obj.pathname : "", + search: obj.search !== undefined ? obj.search : "", + hash: obj.hash !== undefined ? obj.hash : "" + }; + } + } else { + inputsList.push({ + protocol: undefined, + username: undefined, + password: undefined, + hostname: undefined, + port: undefined, + pathname: undefined, + search: undefined, + hash: undefined, + baseURL: undefined + }); + } + + if (candidate.protocol.endsWith(":")) { + candidate.protocol = candidate.protocol.substring(0, candidate.protocol.length - 1); + } + if (candidate.search.startsWith("?")) { + candidate.search = candidate.search.substring(1); + } + if (candidate.hash.startsWith("#")) { + candidate.hash = candidate.hash.substring(1); + } + + const pM = this._protocolPattern.exec(candidate.protocol); + if (!pM.matched) return null; + const uM = this._usernamePattern.exec(candidate.username); + if (!uM.matched) return null; + const pwM = this._passwordPattern.exec(candidate.password); + if (!pwM.matched) return null; + const hM = this._hostnamePattern.exec(candidate.hostname); + if (!hM.matched) return null; + const ptM = this._portPattern.exec(candidate.port); + if (!ptM.matched) return null; + const pathM = this._pathnamePattern.exec(candidate.pathname); + if (!pathM.matched) return null; + const sM = this._searchPattern.exec(candidate.search); + if (!sM.matched) return null; + const hsM = this._hashPattern.exec(candidate.hash); + if (!hsM.matched) return null; + + return { + inputs: inputsList, + protocol: { input: candidate.protocol, groups: pM.groups }, + username: { input: candidate.username, groups: uM.groups }, + password: { input: candidate.password, groups: pwM.groups }, + hostname: { input: candidate.hostname, groups: hM.groups }, + port: { input: candidate.port, groups: ptM.groups }, + pathname: { input: candidate.pathname, groups: pathM.groups }, + search: { input: candidate.search, groups: sM.groups }, + hash: { input: candidate.hash, groups: hsM.groups } + }; + } +} + +export default { + URLPattern +};