mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Merge remote-tracking branch 'origin/opt/native-undici' into opt/npm-polyfill-shims
# Conflicts: # packages/compiler/src/frontend/lowering/lower-modules.ts # tests/harness/npm.test.ts
This commit is contained in:
@@ -35,6 +35,7 @@ output/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
!tests/fixtures/npm-static/node_modules/*/dist/
|
||||
!tests/corpus/*/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -76,7 +76,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu
|
||||
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.
|
||||
|
||||
- `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
|
||||
- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
|
||||
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
|
||||
- `SC6005`: a `Date` string literal uses a form the native parser does not accept.
|
||||
|
||||
@@ -73,6 +73,33 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null
|
||||
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
|
||||
## Built-in packages
|
||||
|
||||
scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual.
|
||||
|
||||
`undici` 5.x and 7.x are built in. The implementation covers `request()`, `Dispatcher` (including subclasses that override `dispatch`), `Client`, `Pool` with its `stats`, `Agent` with per-origin `factory` functions and `connect` options, `buildConnector`, the `errors` classes and their `UND_ERR_*` codes, `setGlobalDispatcher` and `getGlobalDispatcher`, `fetch` and `Headers`, and the request events undici publishes on `node:diagnostics_channel`. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences:
|
||||
|
||||
- A response body supports the readable-stream members undici's body mixin uses: `on`, `once`, `pause`, `resume`, `destroy`, `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `dump()`. It can't be piped or iterated with `for await`.
|
||||
- Diagnostics channel messages carry the request but not the socket or connector.
|
||||
- `fetch` with a dispatcher returns a response whose `url` is empty and whose `redirected` is `false`, offers only `gzip, deflate` encodings, and sends string and byte bodies. Without a dispatcher it is the built-in `fetch`.
|
||||
- Request bodies are strings and bytes; stream, iterable and form-data bodies, `pipelining` above 1, `CONNECT` requests and Unix socket paths aren't supported.
|
||||
|
||||
## Optional dependencies that never load
|
||||
|
||||
Some packages load an optional dependency inside `try`/`catch` and work without it, such as `node-fetch` with `encoding`. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's `package.json`:
|
||||
|
||||
```json:package.json
|
||||
{
|
||||
"scriptc": {
|
||||
"packages": {
|
||||
"encoding": "unloaded"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A `require()` of an unloaded package throws the same `MODULE_NOT_FOUND` error Node.js throws when the package isn't installed, and the package isn't compiled.
|
||||
|
||||
## CommonJS packages
|
||||
|
||||
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
|
||||
|
||||
@@ -2,7 +2,7 @@ A record converted to a record type with a different set of properties, such as
|
||||
|
||||
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
|
||||
|
||||
`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order. `SC6002` reports object literals whose order differs where the program observes it.
|
||||
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. The order matches Node.js when every value of the type is created in the same order. When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it.
|
||||
|
||||
A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. `Object.values` and `Object.entries` check those values against their static result type and throw a `TypeError` if one does not fit. Copies of such a record made by object spread, `Object.assign`, rest destructuring, or conversion to another record type contain only the declared fields. Index-signature records list declared fields before the other keys.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length and string methods use UTF-16 semantics, including for lone surrogates, but relational comparisons such as `<` and `>` use code-point order. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted.
|
||||
Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted.
|
||||
|
||||
One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, combining diacritical marks, and U+2044 FRACTION SLASH): letters order before case and accents, so `"a".localeCompare("B")` is `-1`. Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals. The locales and options arguments are unsupported.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "ee72820d45a79aa154f0",
|
||||
"rowCount": 3880
|
||||
}
|
||||
@@ -208,10 +208,10 @@
|
||||
"entries": 152,
|
||||
"apiEntries": 152,
|
||||
"static": {
|
||||
"supported": 5,
|
||||
"supported": 6,
|
||||
"partial": 39,
|
||||
"refused": 21,
|
||||
"not-implemented": 87,
|
||||
"not-implemented": 86,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -314,9 +314,9 @@
|
||||
"apiEntries": 280,
|
||||
"static": {
|
||||
"supported": 7,
|
||||
"partial": 198,
|
||||
"partial": 200,
|
||||
"refused": 0,
|
||||
"not-implemented": 75,
|
||||
"not-implemented": 73,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -328,10 +328,10 @@
|
||||
"entries": 92,
|
||||
"apiEntries": 92,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"supported": 1,
|
||||
"partial": 15,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"not-implemented": 75,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -853,10 +853,10 @@
|
||||
"entries": 156,
|
||||
"apiEntries": 156,
|
||||
"static": {
|
||||
"supported": 1,
|
||||
"partial": 10,
|
||||
"supported": 2,
|
||||
"partial": 21,
|
||||
"refused": 0,
|
||||
"not-implemented": 145,
|
||||
"not-implemented": 133,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -12325,9 +12325,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "supported",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -30288,9 +30288,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35256,9 +35256,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35693,9 +35693,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36360,9 +36360,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37165,9 +37165,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37303,9 +37303,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "supported",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80183,9 +80183,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "supported",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80229,9 +80229,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80252,9 +80252,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80275,9 +80275,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80298,9 +80298,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80321,9 +80321,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80344,9 +80344,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80436,9 +80436,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80574,9 +80574,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80735,9 +80735,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80758,9 +80758,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80804,9 +80804,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"summary": {
|
||||
"taskCount": 3456,
|
||||
"tierItemCount": 3456,
|
||||
"taskCount": 3453,
|
||||
"tierItemCount": 3453,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1163,
|
||||
"verify-gap": 2165,
|
||||
"audit-partial": 1179,
|
||||
"verify-gap": 2146,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
},
|
||||
"priorities": {
|
||||
"high": 2289,
|
||||
"high": 2287,
|
||||
"normal": 694,
|
||||
"low": 473
|
||||
"low": 472
|
||||
}
|
||||
},
|
||||
"tasks": [
|
||||
@@ -7359,31 +7359,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "crypto:2ab8394e29a7",
|
||||
"chapter": "crypto",
|
||||
"label": "crypto.getRandomValues(typedArray)",
|
||||
"signature": "`crypto.getRandomValues(typedArray)`",
|
||||
"apiSymbol": "crypto.getRandomValues",
|
||||
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/crypto.html#cryptogetrandomvaluestypedarray",
|
||||
"nodeStability": {
|
||||
"index": "2",
|
||||
"level": 2,
|
||||
"text": "Stable",
|
||||
"inherited": true
|
||||
},
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:crypto",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "crypto:21f22c71fb61",
|
||||
"chapter": "crypto",
|
||||
@@ -27067,12 +27042,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:fs",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.fs.constants",
|
||||
"tests": [
|
||||
"tests/corpus/6207-fs-constants-object.ts",
|
||||
"tests/corpus/6208-fs-constants-require.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -32764,12 +32742,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:fs",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.fs.constants",
|
||||
"tests": [
|
||||
"tests/corpus/6207-fs-constants-object.ts",
|
||||
"tests/corpus/6208-fs-constants-require.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -33220,12 +33201,18 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:globals.CryptoKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -33951,12 +33938,18 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:globals.SubtleCrypto",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -34872,12 +34865,16 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:globals.navigator",
|
||||
"tests": [
|
||||
"tests/fixtures/node-types/navigator.ts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/6331-jose-root-entry/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -35006,31 +35003,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "globals:fc0a32e8c26f",
|
||||
"chapter": "globals",
|
||||
"label": "userAgent",
|
||||
"signature": "`userAgent` Type: {string}",
|
||||
"apiSymbol": "userAgent",
|
||||
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator",
|
||||
"nodeStability": {
|
||||
"index": "1.1",
|
||||
"level": 1,
|
||||
"text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.",
|
||||
"inherited": true
|
||||
},
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "globals:b412bf9a456d",
|
||||
"chapter": "globals",
|
||||
@@ -81027,31 +80999,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "webcrypto:dcc24f369f43",
|
||||
"chapter": "webcrypto",
|
||||
"label": "crypto.getRandomValues(typedArray)",
|
||||
"signature": "`crypto.getRandomValues(typedArray)`",
|
||||
"apiSymbol": "crypto.getRandomValues",
|
||||
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/webcrypto.html#cryptogetrandomvaluestypedarray",
|
||||
"nodeStability": {
|
||||
"index": "2",
|
||||
"level": 2,
|
||||
"text": "Stable",
|
||||
"inherited": true
|
||||
},
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "webcrypto:8c570ec75666",
|
||||
"chapter": "webcrypto",
|
||||
@@ -81068,12 +81015,18 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.subtle",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81093,12 +81046,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.CryptoKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81118,12 +81075,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.cryptoKey.algorithm",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81143,12 +81104,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.extractable",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81168,12 +81133,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.type",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81193,12 +81162,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.usages",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81293,12 +81266,18 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.SubtleCrypto",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81443,12 +81422,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.subtle.digest",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81618,12 +81604,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.subtle.importKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81643,12 +81636,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.subtle.sign",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81693,12 +81693,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webcrypto",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webcrypto.subtle.verify",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -190,10 +190,10 @@
|
||||
"entries": 179,
|
||||
"apiEntries": 152,
|
||||
"static": {
|
||||
"supported": 5,
|
||||
"supported": 6,
|
||||
"partial": 39,
|
||||
"refused": 21,
|
||||
"not-implemented": 87,
|
||||
"not-implemented": 86,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -326,9 +326,9 @@
|
||||
"apiEntries": 280,
|
||||
"static": {
|
||||
"supported": 7,
|
||||
"partial": 198,
|
||||
"partial": 200,
|
||||
"refused": 0,
|
||||
"not-implemented": 75,
|
||||
"not-implemented": 73,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -340,10 +340,10 @@
|
||||
"entries": 94,
|
||||
"apiEntries": 92,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"supported": 1,
|
||||
"partial": 15,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"not-implemented": 75,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -910,10 +910,10 @@
|
||||
"entries": 176,
|
||||
"apiEntries": 156,
|
||||
"static": {
|
||||
"supported": 1,
|
||||
"partial": 10,
|
||||
"supported": 2,
|
||||
"partial": 21,
|
||||
"refused": 0,
|
||||
"not-implemented": 145,
|
||||
"not-implemented": 133,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -19872,6 +19872,8 @@
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/6204-crypto-get-random-values.ts",
|
||||
"tests/corpus/6205-crypto-get-random-values.js",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
@@ -21028,8 +21030,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "supported",
|
||||
"evidence": "compiler-dedicated:crypto.getRandomValues",
|
||||
"tests": [
|
||||
"tests/corpus/6204-crypto-get-random-values.ts",
|
||||
"tests/corpus/6205-crypto-get-random-values.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -44138,6 +44144,8 @@
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts",
|
||||
"tests/corpus/6207-fs-constants-object.ts",
|
||||
"tests/corpus/6208-fs-constants-require.cjs",
|
||||
"tests/corpus/fs-native-realpath.ts",
|
||||
"tests/corpus/node24-fs-links-callbacks.cjs",
|
||||
"tests/corpus/node24-fs-links-errors.cjs",
|
||||
@@ -45925,8 +45933,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:fs"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.fs.constants",
|
||||
"tests": [
|
||||
"tests/corpus/6207-fs-constants-object.ts",
|
||||
"tests/corpus/6208-fs-constants-require.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -51797,8 +51809,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:fs"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.fs.constants",
|
||||
"tests": [
|
||||
"tests/corpus/6207-fs-constants-object.ts",
|
||||
"tests/corpus/6208-fs-constants-require.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -52226,8 +52242,16 @@
|
||||
"tests/corpus/5288-blob-validation.js",
|
||||
"tests/corpus/5290-blob-dynamic.js",
|
||||
"tests/corpus/5830-timers-promises-signal.ts",
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/corpus/6331-jose-root-entry/main.ts",
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/harness/fetch-conformance.test.ts"
|
||||
"tests/fixtures/node-types/navigator.ts",
|
||||
"tests/harness/fetch-conformance.test.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -52648,8 +52672,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:globals.CryptoKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -53322,8 +53353,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:globals.SubtleCrypto",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -54228,8 +54266,13 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:globals.navigator",
|
||||
"tests": [
|
||||
"tests/fixtures/node-types/navigator.ts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/6331-jose-root-entry/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -54366,8 +54409,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "supported",
|
||||
"evidence": "compiler-feature:globals.userAgent",
|
||||
"tests": [
|
||||
"tests/fixtures/node-types/navigator.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -106854,10 +106901,18 @@
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
"tests/corpus/5442-crypto-random-values.ts",
|
||||
"tests/corpus/5443-crypto-random-arguments.js",
|
||||
"tests/corpus/6204-crypto-get-random-values.ts",
|
||||
"tests/corpus/6205-crypto-get-random-values.js",
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
"tests/corpus/node24-crypto-sha2.ts",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts",
|
||||
"tests/harness/npm-static.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -110338,8 +110393,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "supported",
|
||||
"evidence": "compiler-dedicated:crypto.getRandomValues",
|
||||
"tests": [
|
||||
"tests/corpus/6204-crypto-get-random-values.ts",
|
||||
"tests/corpus/6205-crypto-get-random-values.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110389,8 +110448,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.subtle",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -110412,8 +110478,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.CryptoKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110435,8 +110506,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.cryptoKey.algorithm",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110458,8 +110534,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.extractable",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -110481,8 +110562,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.type",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -110504,8 +110590,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.usages",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -110596,8 +110687,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.SubtleCrypto",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110734,8 +110832,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.subtle.digest",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110895,8 +111001,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.subtle.importKey",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110918,8 +111032,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.subtle.sign",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -110964,8 +111086,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webcrypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webcrypto.subtle.verify",
|
||||
"tests": [
|
||||
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
|
||||
"tests/corpus/6292-webcrypto-hmac-errors.ts",
|
||||
"tests/corpus/6293-webcrypto-digest.ts",
|
||||
"tests/corpus/6294-webcrypto-js-values.mjs",
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/fixtures/node-types/webcrypto-hmac.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
"node-builtin.os.uptime": ["tests/corpus/5140-os-host-facts.ts"],
|
||||
"node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"],
|
||||
"node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"],
|
||||
"node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"],
|
||||
"node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"],
|
||||
"node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
"node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
"node-builtin.url.resolve": ["tests/corpus/5226-url-resolve.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
@@ -177,11 +179,19 @@
|
||||
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
|
||||
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
|
||||
"stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
|
||||
"stdlib.response": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
|
||||
},
|
||||
"features": [
|
||||
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] },
|
||||
{"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]},
|
||||
{"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]},
|
||||
@@ -391,6 +401,7 @@
|
||||
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
|
||||
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
|
||||
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
|
||||
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
|
||||
|
||||
+173
-6
@@ -511,14 +511,119 @@ declare var DOMException: {
|
||||
readonly prototype: DOMException;
|
||||
};
|
||||
|
||||
/* Capability-gated globals harness code probes behind feature guards
|
||||
* (globalThis.crypto under a hasCrypto check, the SQLite-backed storage
|
||||
* pair): typed `unknown` — no lowering exists, the guard is expected to
|
||||
* be false in a compiled binary, and any reached use fences per site. */
|
||||
declare var crypto: unknown;
|
||||
/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for
|
||||
* raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as
|
||||
* exactly that surface (the declarations mirror @types/node's webcrypto
|
||||
* shapes). Every other SubtleCrypto method and crypto member is absent,
|
||||
* so a use is an honest type error. CryptoKey values are native handles
|
||||
* whose four members read through the handle. */
|
||||
declare var crypto: Crypto;
|
||||
/* The Crypto and SubtleCrypto constructors (feature probes only) stay
|
||||
* `unknown`; the values keep this declaration order. */
|
||||
declare var Crypto: unknown;
|
||||
declare var CryptoKey: unknown;
|
||||
declare var CryptoKey: {
|
||||
prototype: CryptoKey;
|
||||
new (): CryptoKey;
|
||||
};
|
||||
declare var SubtleCrypto: unknown;
|
||||
type KeyUsage =
|
||||
| "encrypt"
|
||||
| "decrypt"
|
||||
| "sign"
|
||||
| "verify"
|
||||
| "deriveKey"
|
||||
| "deriveBits"
|
||||
| "encapsulateBits"
|
||||
| "decapsulateBits"
|
||||
| "encapsulateKey"
|
||||
| "decapsulateKey"
|
||||
| "wrapKey"
|
||||
| "unwrapKey";
|
||||
type KeyType = "private" | "public" | "secret";
|
||||
interface Algorithm {
|
||||
name: string;
|
||||
}
|
||||
type AlgorithmIdentifier = Algorithm | string;
|
||||
type HashAlgorithmIdentifier = AlgorithmIdentifier;
|
||||
interface HmacImportParams extends Algorithm {
|
||||
hash: HashAlgorithmIdentifier;
|
||||
length?: number;
|
||||
}
|
||||
interface KeyAlgorithm {
|
||||
name: string;
|
||||
}
|
||||
interface HmacKeyAlgorithm extends KeyAlgorithm {
|
||||
hash: KeyAlgorithm;
|
||||
length: number;
|
||||
}
|
||||
interface CryptoKey {
|
||||
readonly algorithm: KeyAlgorithm;
|
||||
readonly extractable: boolean;
|
||||
readonly type: KeyType;
|
||||
readonly usages: KeyUsage[];
|
||||
}
|
||||
type KeyFormat =
|
||||
| "jwk"
|
||||
| "pkcs8"
|
||||
| "raw"
|
||||
| "raw-private"
|
||||
| "raw-public"
|
||||
| "raw-secret"
|
||||
| "raw-seed"
|
||||
| "spki";
|
||||
interface JsonWebKey {
|
||||
alg?: string;
|
||||
crv?: string;
|
||||
d?: string;
|
||||
dp?: string;
|
||||
dq?: string;
|
||||
e?: string;
|
||||
ext?: boolean;
|
||||
k?: string;
|
||||
key_ops?: string[];
|
||||
kty?: string;
|
||||
n?: string;
|
||||
p?: string;
|
||||
q?: string;
|
||||
qi?: string;
|
||||
use?: string;
|
||||
x?: string;
|
||||
y?: string;
|
||||
}
|
||||
/* The 'jwk' form type-checks like @types/node's; the compiler refuses it
|
||||
* (the JsonWebKey import is not implemented). */
|
||||
interface SubtleCrypto {
|
||||
digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>;
|
||||
importKey(
|
||||
format: "jwk",
|
||||
keyData: JsonWebKey,
|
||||
algorithm: AlgorithmIdentifier | HmacImportParams,
|
||||
extractable: boolean,
|
||||
keyUsages: readonly KeyUsage[],
|
||||
): Promise<CryptoKey>;
|
||||
importKey(
|
||||
format: Exclude<KeyFormat, "jwk">,
|
||||
keyData: BufferSource,
|
||||
algorithm: AlgorithmIdentifier | HmacImportParams,
|
||||
extractable: boolean,
|
||||
keyUsages: readonly KeyUsage[],
|
||||
): Promise<CryptoKey>;
|
||||
sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
|
||||
verify(
|
||||
algorithm: AlgorithmIdentifier,
|
||||
key: CryptoKey,
|
||||
signature: BufferSource,
|
||||
data: BufferSource,
|
||||
): Promise<boolean>;
|
||||
}
|
||||
interface Crypto {
|
||||
readonly subtle: SubtleCrypto;
|
||||
}
|
||||
|
||||
/* Capability-gated globals harness code probes behind feature guards
|
||||
* (the SQLite-backed storage pair): typed `unknown` — no lowering exists,
|
||||
* the guard is expected to be false in a compiled binary, and any reached
|
||||
* use fences per site. */
|
||||
declare var localStorage: unknown;
|
||||
declare var sessionStorage: unknown;
|
||||
|
||||
@@ -1493,6 +1598,51 @@ declare module "node:fs" {
|
||||
readonly COPYFILE_EXCL: number;
|
||||
readonly COPYFILE_FICLONE: number;
|
||||
readonly COPYFILE_FICLONE_FORCE: number;
|
||||
readonly UV_FS_SYMLINK_DIR: number;
|
||||
readonly UV_FS_SYMLINK_JUNCTION: number;
|
||||
readonly UV_DIRENT_UNKNOWN: number;
|
||||
readonly UV_DIRENT_FILE: number;
|
||||
readonly UV_DIRENT_DIR: number;
|
||||
readonly UV_DIRENT_LINK: number;
|
||||
readonly UV_DIRENT_FIFO: number;
|
||||
readonly UV_DIRENT_SOCKET: number;
|
||||
readonly UV_DIRENT_CHAR: number;
|
||||
readonly UV_DIRENT_BLOCK: number;
|
||||
readonly UV_FS_O_FILEMAP: number;
|
||||
readonly UV_FS_O_TEMPORARY: number;
|
||||
readonly UV_FS_O_SHORT_LIVED: number;
|
||||
readonly UV_FS_O_SEQUENTIAL: number;
|
||||
readonly UV_FS_O_RANDOM: number;
|
||||
readonly UV_FS_COPYFILE_EXCL: number;
|
||||
readonly UV_FS_COPYFILE_FICLONE: number;
|
||||
readonly UV_FS_COPYFILE_FICLONE_FORCE: number;
|
||||
readonly S_IFMT: number;
|
||||
readonly S_IFREG: number;
|
||||
readonly S_IFDIR: number;
|
||||
readonly S_IFCHR: number;
|
||||
readonly S_IFBLK: number;
|
||||
readonly S_IFIFO: number;
|
||||
readonly S_IFLNK: number;
|
||||
readonly S_IFSOCK: number;
|
||||
readonly O_NOCTTY: number;
|
||||
readonly O_DIRECTORY: number;
|
||||
readonly O_NOATIME: number;
|
||||
readonly O_SYNC: number;
|
||||
readonly O_DSYNC: number;
|
||||
readonly O_SYMLINK: number;
|
||||
readonly O_DIRECT: number;
|
||||
readonly S_IRWXU: number;
|
||||
readonly S_IRUSR: number;
|
||||
readonly S_IWUSR: number;
|
||||
readonly S_IXUSR: number;
|
||||
readonly S_IRWXG: number;
|
||||
readonly S_IRGRP: number;
|
||||
readonly S_IWGRP: number;
|
||||
readonly S_IXGRP: number;
|
||||
readonly S_IRWXO: number;
|
||||
readonly S_IROTH: number;
|
||||
readonly S_IWOTH: number;
|
||||
readonly S_IXOTH: number;
|
||||
};
|
||||
/* A stat(2) snapshot (statSync follows symlinks, lstatSync does not —
|
||||
* Node's split) — immutable; the supported surface is exactly these
|
||||
@@ -2557,6 +2707,23 @@ declare module "crypto" {
|
||||
a: ArrayBufferView | ArrayBufferLike,
|
||||
b: ArrayBufferView | ArrayBufferLike,
|
||||
): boolean;
|
||||
/* WebCrypto's getRandomValues (also the webcrypto object's): integer
|
||||
* typed arrays, filled in place and returned. The crypto GLOBAL stays
|
||||
* unknown in fallback builds. */
|
||||
type RandomValuesArray =
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| BigInt64Array
|
||||
| BigUint64Array;
|
||||
export function getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
|
||||
export const webcrypto: {
|
||||
getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
|
||||
};
|
||||
export function randomFillSync<T extends ArrayBufferView | ArrayBufferLike>(
|
||||
buffer: T,
|
||||
offset?: number,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"ambient",
|
||||
"shims",
|
||||
"surface-manifest.json"
|
||||
],
|
||||
"publishConfig": {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// undici's Agent (lib/dispatcher/agent.js): one dispatcher per origin,
|
||||
// created by `factory` (default: a Pool, or a Client when connections is 1).
|
||||
import { Client } from "./client.ts";
|
||||
import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
|
||||
import { InvalidArgumentError } from "./errors.ts";
|
||||
import { Pool } from "./pool.ts";
|
||||
import type {
|
||||
AgentOptions,
|
||||
DispatchHandlers,
|
||||
DispatchOptions,
|
||||
PoolOptions,
|
||||
Profile,
|
||||
} from "./types.ts";
|
||||
|
||||
interface AgentEntry {
|
||||
dispatcher: Dispatcher;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class Agent extends DispatcherBase {
|
||||
__options: PoolOptions;
|
||||
__factory: ((origin: string | URL, opts: object) => Dispatcher) | null;
|
||||
__clients: Map<string, AgentEntry>;
|
||||
__onDrain: (origin: URL, targets: Dispatcher[]) => void;
|
||||
__onConnect: (origin: URL, targets: Dispatcher[]) => void;
|
||||
__onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
|
||||
__onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
|
||||
|
||||
constructor(options: AgentOptions | undefined, profile: Profile) {
|
||||
super(profile);
|
||||
const opts: AgentOptions = options ?? {};
|
||||
const interceptors = opts.interceptors;
|
||||
if (profile.major < 7 && interceptors !== undefined && interceptors.Agent !== undefined) {
|
||||
this.__interceptors = interceptors.Agent;
|
||||
}
|
||||
this.__options = {
|
||||
keepAliveTimeout: opts.keepAliveTimeout,
|
||||
keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
|
||||
keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
|
||||
pipelining: opts.pipelining,
|
||||
maxHeaderSize: opts.maxHeaderSize,
|
||||
bodyTimeout: opts.bodyTimeout,
|
||||
headersTimeout: opts.headersTimeout,
|
||||
strictContentLength: opts.strictContentLength,
|
||||
maxRequestsPerClient: opts.maxRequestsPerClient,
|
||||
maxResponseSize: opts.maxResponseSize,
|
||||
connectTimeout: opts.connectTimeout,
|
||||
localAddress: opts.localAddress,
|
||||
connections: opts.connections,
|
||||
connect: opts.connect,
|
||||
interceptors:
|
||||
interceptors !== undefined
|
||||
? { Pool: interceptors.Pool, Client: interceptors.Client }
|
||||
: undefined,
|
||||
};
|
||||
const factory = opts.factory;
|
||||
this.__factory = factory !== undefined ? factory : null;
|
||||
this.__clients = new Map<string, AgentEntry>();
|
||||
this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
|
||||
this.emit("drain", origin, [this as Dispatcher, ...targets]);
|
||||
};
|
||||
this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
|
||||
this.emit("connect", origin, [this as Dispatcher, ...targets]);
|
||||
};
|
||||
this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
|
||||
this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
|
||||
};
|
||||
this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
|
||||
this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
|
||||
};
|
||||
}
|
||||
|
||||
__runningCount(): number {
|
||||
let ret = 0;
|
||||
for (const entry of this.__clients.values()) ret += entry.dispatcher.__runningCount();
|
||||
return ret;
|
||||
}
|
||||
|
||||
__dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
|
||||
const origin = opts.origin;
|
||||
if (origin === undefined || origin === "") {
|
||||
throw new InvalidArgumentError("opts.origin must be a non-empty string or URL.");
|
||||
}
|
||||
const key = typeof origin === "string" ? origin : origin.href;
|
||||
const existing = this.__clients.get(key);
|
||||
let dispatcher: Dispatcher;
|
||||
if (existing !== undefined) {
|
||||
dispatcher = existing.dispatcher;
|
||||
} else {
|
||||
const factory = this.__factory;
|
||||
dispatcher =
|
||||
factory !== null
|
||||
? factory(origin, this.__options)
|
||||
: this.__defaultFactory(origin, this.__options);
|
||||
const entry: AgentEntry = { dispatcher, count: 0 };
|
||||
if (this.__profile.major >= 7) {
|
||||
// 7.x closes and forgets an origin's dispatcher once all of its
|
||||
// connections are gone.
|
||||
const closeIfUnused = (connected: boolean): void => {
|
||||
const current = this.__clients.get(key);
|
||||
if (current !== undefined) {
|
||||
if (connected) current.count -= 1;
|
||||
if (current.count <= 0) {
|
||||
this.__clients.delete(key);
|
||||
if (!current.dispatcher.__isDestroyed()) current.dispatcher.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
dispatcher.on("drain", this.__onDrain);
|
||||
dispatcher.on("connect", (o: URL, targets: Dispatcher[]) => {
|
||||
const current = this.__clients.get(key);
|
||||
if (current !== undefined) current.count += 1;
|
||||
this.__onConnect(o, targets);
|
||||
});
|
||||
dispatcher.on("disconnect", (o: URL, targets: Dispatcher[], err: Error) => {
|
||||
closeIfUnused(true);
|
||||
this.__onDisconnect(o, targets, err);
|
||||
});
|
||||
dispatcher.on("connectionError", (o: URL, targets: Dispatcher[], err: Error) => {
|
||||
closeIfUnused(false);
|
||||
this.__onConnectionError(o, targets, err);
|
||||
});
|
||||
} else {
|
||||
dispatcher.on("drain", this.__onDrain);
|
||||
dispatcher.on("connect", this.__onConnect);
|
||||
dispatcher.on("disconnect", this.__onDisconnect);
|
||||
dispatcher.on("connectionError", this.__onConnectionError);
|
||||
}
|
||||
this.__clients.set(key, entry);
|
||||
}
|
||||
return dispatcher.dispatch(opts, handler);
|
||||
}
|
||||
|
||||
/** The default factory: a Client when `connections` is 1, else a Pool, of
|
||||
* the same undici version. */
|
||||
__defaultFactory(origin: string | URL, opts: PoolOptions): Dispatcher {
|
||||
return opts.connections === 1
|
||||
? this.__createClient(origin, opts)
|
||||
: this.__createPool(origin, opts);
|
||||
}
|
||||
|
||||
__createClient(origin: string | URL, opts: PoolOptions): Dispatcher {
|
||||
return new Client(origin, opts, this.__profile);
|
||||
}
|
||||
|
||||
__createPool(origin: string | URL, opts: PoolOptions): Dispatcher {
|
||||
return new Pool(origin, opts, this.__profile);
|
||||
}
|
||||
|
||||
__close(): Promise<void> {
|
||||
const closing: Promise<void>[] = [];
|
||||
for (const entry of this.__clients.values()) closing.push(entry.dispatcher.close());
|
||||
if (this.__profile.major >= 7) this.__clients.clear();
|
||||
return Promise.all(closing).then(() => {});
|
||||
}
|
||||
|
||||
__destroy(err: Error): Promise<void> {
|
||||
const destroying: Promise<void>[] = [];
|
||||
for (const entry of this.__clients.values()) destroying.push(entry.dispatcher.destroy(err));
|
||||
if (this.__profile.major >= 7) this.__clients.clear();
|
||||
return Promise.all(destroying).then(() => {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// undici's top-level request() (index.js makeDispatcher over
|
||||
// lib/api/api-request.js) and the global dispatcher
|
||||
// (lib/global.js: one process-wide slot shared by every undici copy).
|
||||
import { requestThrough } from "./dispatcher.ts";
|
||||
import type { Dispatcher, RequestOptions, ResponseData } from "./dispatcher.ts";
|
||||
import { parseOrigin, parseURL } from "./util.ts";
|
||||
import type { IncomingHttpHeaders, Profile, RequestBody, UndiciHeaders } from "./types.ts";
|
||||
|
||||
/** request(url, options)'s options: Dispatcher.RequestOptions without
|
||||
* origin/path, an optional method, and the dispatcher to use. */
|
||||
export interface TopLevelRequestOptions {
|
||||
dispatcher?: Dispatcher;
|
||||
method?: string;
|
||||
path?: string;
|
||||
body?: RequestBody;
|
||||
headers?: UndiciHeaders;
|
||||
idempotent?: boolean;
|
||||
blocking?: boolean;
|
||||
headersTimeout?: number | null;
|
||||
bodyTimeout?: number | null;
|
||||
reset?: boolean;
|
||||
throwOnError?: boolean;
|
||||
opaque?: unknown;
|
||||
signal?: AbortSignal | null;
|
||||
onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
|
||||
highWaterMark?: number;
|
||||
}
|
||||
|
||||
let globalDispatcher: Dispatcher | null = null;
|
||||
let globalDispatcherSet = false;
|
||||
|
||||
export function setGlobalDispatcher(dispatcher: Dispatcher): void {
|
||||
globalDispatcher = dispatcher;
|
||||
globalDispatcherSet = true;
|
||||
}
|
||||
|
||||
/** The global dispatcher, created on first use by the asking version's
|
||||
* Agent (undici stores whichever copy asked first). */
|
||||
export function getGlobalDispatcher(create: () => Dispatcher): Dispatcher {
|
||||
let dispatcher = globalDispatcher;
|
||||
if (dispatcher === null) {
|
||||
dispatcher = create();
|
||||
globalDispatcher = dispatcher;
|
||||
}
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
/** True once a program installed its own global dispatcher. */
|
||||
export function hasCustomGlobalDispatcher(): boolean {
|
||||
return globalDispatcherSet;
|
||||
}
|
||||
|
||||
export function request(
|
||||
url: string | URL,
|
||||
options: TopLevelRequestOptions | undefined,
|
||||
createGlobal: () => Dispatcher,
|
||||
profile: Profile,
|
||||
): Promise<ResponseData> {
|
||||
// makeDispatcher validates synchronously: an invalid URL throws at the
|
||||
// call rather than rejecting.
|
||||
{
|
||||
const opts: TopLevelRequestOptions = options ?? {};
|
||||
let target: URL;
|
||||
const path = opts.path;
|
||||
if (path !== undefined) {
|
||||
const prefixed = path.startsWith("/") ? path : `/${path}`;
|
||||
target = new URL(parseOrigin(url, profile).origin + prefixed);
|
||||
} else {
|
||||
target = parseURL(url, profile);
|
||||
}
|
||||
const dispatcher = opts.dispatcher ?? getGlobalDispatcher(createGlobal);
|
||||
const method =
|
||||
opts.method !== undefined && opts.method !== "" ? opts.method : opts.body ? "PUT" : "GET";
|
||||
const dispatchOptions: RequestOptions = {
|
||||
origin: target.origin,
|
||||
path: target.search ? `${target.pathname}${target.search}` : target.pathname,
|
||||
method,
|
||||
body: opts.body,
|
||||
headers: opts.headers,
|
||||
idempotent: opts.idempotent,
|
||||
blocking: opts.blocking,
|
||||
headersTimeout: opts.headersTimeout,
|
||||
bodyTimeout: opts.bodyTimeout,
|
||||
reset: opts.reset,
|
||||
throwOnError: opts.throwOnError,
|
||||
opaque: opts.opaque,
|
||||
signal: opts.signal,
|
||||
onInfo: opts.onInfo,
|
||||
highWaterMark: opts.highWaterMark,
|
||||
};
|
||||
return requestThrough(dispatcher, dispatchOptions, profile);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
// undici's buildConnector (lib/core/connect.js) over node:net and node:tls.
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
import { ConnectTimeoutError, InvalidArgumentError, NotSupportedError } from "./errors.ts";
|
||||
import type {
|
||||
Connector,
|
||||
ConnectorBuildOptions,
|
||||
ConnectorCallback,
|
||||
ConnectorOptions,
|
||||
LookupAddress,
|
||||
} from "./types.ts";
|
||||
|
||||
export function buildConnector(options: ConnectorBuildOptions): Connector {
|
||||
const maxCachedSessions = options.maxCachedSessions;
|
||||
if (
|
||||
maxCachedSessions !== undefined &&
|
||||
maxCachedSessions !== null &&
|
||||
(!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)
|
||||
) {
|
||||
throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero");
|
||||
}
|
||||
if (options.socketPath !== undefined && options.socketPath !== null) {
|
||||
throw new NotSupportedError("scriptc's undici connector does not dial socketPath yet");
|
||||
}
|
||||
const timeout =
|
||||
options.timeout === undefined || options.timeout === null ? 10e3 : options.timeout;
|
||||
const lookup = options.lookup;
|
||||
// The plain-TCP dial, with the caller's resolver when one is configured.
|
||||
let dialTcp: (port: number, host: string) => net.Socket;
|
||||
if (lookup !== undefined) {
|
||||
// net.connect's lowered resolver answers every address at once; adapt a
|
||||
// resolver that answers a single address string.
|
||||
const userLookup: (
|
||||
hostname: string,
|
||||
options: unknown,
|
||||
callback: (
|
||||
err: NodeJS.ErrnoException | null,
|
||||
address: string | LookupAddress[],
|
||||
family?: number,
|
||||
) => void,
|
||||
) => void = lookup;
|
||||
const resolver = (
|
||||
hostname: string,
|
||||
lookupOptions: unknown,
|
||||
callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void,
|
||||
): void => {
|
||||
userLookup(hostname, lookupOptions, (err, address, family) => {
|
||||
if (err !== null) callback(err, []);
|
||||
else if (typeof address === "string") {
|
||||
callback(null, [{ address, family: family ?? (net.isIPv6(address) ? 6 : 4) }]);
|
||||
} else callback(null, address);
|
||||
});
|
||||
};
|
||||
dialTcp = (port: number, host: string): net.Socket =>
|
||||
net.connect({ port, host, autoSelectFamily: true, lookup: resolver });
|
||||
} else {
|
||||
dialTcp = (port: number, host: string): net.Socket => net.connect({ port, host });
|
||||
}
|
||||
const rejectUnauthorized = options.rejectUnauthorized;
|
||||
const ca = options.ca;
|
||||
const defaultServername = options.servername;
|
||||
|
||||
return (params: ConnectorOptions, callback: ConnectorCallback): net.Socket => {
|
||||
let pending: ConnectorCallback | null = callback;
|
||||
const https = params.protocol === "https:";
|
||||
const port = params.port !== "" ? Number(params.port) : https ? 443 : 80;
|
||||
let socket: net.Socket;
|
||||
if (https) {
|
||||
if (lookup !== undefined) {
|
||||
throw new NotSupportedError(
|
||||
"scriptc's undici connector does not apply a custom lookup to TLS connections yet",
|
||||
);
|
||||
}
|
||||
const servername =
|
||||
params.servername !== undefined && params.servername !== ""
|
||||
? params.servername
|
||||
: defaultServername !== undefined
|
||||
? defaultServername
|
||||
: serverNameOf(params.host ?? params.hostname);
|
||||
socket = tls.connect({
|
||||
port,
|
||||
host: params.hostname,
|
||||
servername,
|
||||
rejectUnauthorized: rejectUnauthorized ?? true,
|
||||
...(ca !== undefined ? { ca } : {}),
|
||||
});
|
||||
} else {
|
||||
socket = dialTcp(port, params.hostname);
|
||||
}
|
||||
|
||||
let cancelTimer: (() => void) | null = null;
|
||||
if (timeout !== 0) {
|
||||
const timer = setTimeout(() => {
|
||||
cancelTimer = null;
|
||||
setImmediate(() => {
|
||||
const err = new ConnectTimeoutError();
|
||||
socket.destroy();
|
||||
if (pending !== null) {
|
||||
const cb = pending;
|
||||
pending = null;
|
||||
cb(err, null);
|
||||
}
|
||||
});
|
||||
}, timeout);
|
||||
cancelTimer = () => clearTimeout(timer);
|
||||
}
|
||||
const cancelTimeout = (): void => {
|
||||
const cancel = cancelTimer;
|
||||
cancelTimer = null;
|
||||
if (cancel !== null) cancel();
|
||||
};
|
||||
|
||||
socket.setNoDelay(true);
|
||||
const onConnected = (): void => {
|
||||
cancelTimeout();
|
||||
if (pending !== null) {
|
||||
const cb = pending;
|
||||
pending = null;
|
||||
cb(null, socket);
|
||||
}
|
||||
};
|
||||
if (https) socket.once("secureConnect", onConnected);
|
||||
else socket.once("connect", onConnected);
|
||||
socket.on("error", (err: Error) => {
|
||||
cancelTimeout();
|
||||
if (pending !== null) {
|
||||
const cb = pending;
|
||||
pending = null;
|
||||
cb(err, null);
|
||||
}
|
||||
});
|
||||
return socket;
|
||||
};
|
||||
}
|
||||
|
||||
/** undici's util.getServerName: the host without its port, or null for an
|
||||
* IP literal (no SNI). */
|
||||
function serverNameOf(host: string): string | undefined {
|
||||
if (host === "") return undefined;
|
||||
let servername = host;
|
||||
if (servername.startsWith("[")) {
|
||||
const idx = servername.indexOf("]");
|
||||
servername = idx === -1 ? servername : servername.slice(1, idx);
|
||||
} else {
|
||||
const idx = servername.indexOf(":");
|
||||
if (idx !== -1) servername = servername.slice(0, idx);
|
||||
}
|
||||
return net.isIP(servername) !== 0 ? undefined : servername;
|
||||
}
|
||||
|
||||
/** The `buildConnector.*` type namespace of undici's types/connector.d.ts. */
|
||||
export declare namespace buildConnector {
|
||||
type BuildOptions = ConnectorBuildOptions;
|
||||
type Options = ConnectorOptions;
|
||||
type Callback = ConnectorCallback;
|
||||
type connector = Connector;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// undici's DiagnosticsChannel message types (types/diagnostics-channel.d.ts).
|
||||
import type { Socket } from "node:net";
|
||||
import type { Connector } from "./types.ts";
|
||||
|
||||
export declare namespace DiagnosticsChannel {
|
||||
interface Request {
|
||||
origin?: string | URL;
|
||||
completed: boolean;
|
||||
method?: string;
|
||||
path: string;
|
||||
headers: string;
|
||||
addHeader(key: string, value: string): Request;
|
||||
}
|
||||
interface Response {
|
||||
statusCode: number;
|
||||
statusText: string;
|
||||
headers: Array<Buffer>;
|
||||
}
|
||||
type Error = unknown;
|
||||
interface ConnectParams {
|
||||
host: string;
|
||||
hostname: string;
|
||||
protocol: string;
|
||||
port: string;
|
||||
servername: string | null;
|
||||
}
|
||||
type Connector = import("./types.ts").Connector;
|
||||
interface RequestCreateMessage {
|
||||
request: Request;
|
||||
}
|
||||
interface RequestBodySentMessage {
|
||||
request: Request;
|
||||
}
|
||||
interface RequestHeadersMessage {
|
||||
request: Request;
|
||||
response: Response;
|
||||
}
|
||||
interface RequestTrailersMessage {
|
||||
request: Request;
|
||||
trailers: Array<Buffer>;
|
||||
}
|
||||
interface RequestErrorMessage {
|
||||
request: Request;
|
||||
error: Error;
|
||||
}
|
||||
interface ClientSendHeadersMessage {
|
||||
request: Request;
|
||||
headers: string;
|
||||
socket: Socket;
|
||||
}
|
||||
interface ClientBeforeConnectMessage {
|
||||
connectParams: ConnectParams;
|
||||
connector: Connector;
|
||||
}
|
||||
interface ClientConnectedMessage {
|
||||
socket: Socket;
|
||||
connectParams: ConnectParams;
|
||||
connector: Connector;
|
||||
}
|
||||
interface ClientConnectErrorMessage {
|
||||
error: Error;
|
||||
socket: Socket;
|
||||
connectParams: ConnectParams;
|
||||
connector: Connector;
|
||||
}
|
||||
}
|
||||
|
||||
export type { Connector };
|
||||
@@ -0,0 +1,16 @@
|
||||
// undici's diagnostics_channel publications (lib/core/diagnostics.js). The
|
||||
// channel names and message shapes are undici's; subscribers see the same
|
||||
// request objects across one request's lifetime.
|
||||
import { channel } from "node:diagnostics_channel";
|
||||
|
||||
export const channels = {
|
||||
create: channel("undici:request:create"),
|
||||
bodySent: channel("undici:request:bodySent"),
|
||||
headers: channel("undici:request:headers"),
|
||||
trailers: channel("undici:request:trailers"),
|
||||
error: channel("undici:request:error"),
|
||||
beforeConnect: channel("undici:client:beforeConnect"),
|
||||
connected: channel("undici:client:connected"),
|
||||
connectError: channel("undici:client:connectError"),
|
||||
sendHeaders: channel("undici:client:sendHeaders"),
|
||||
};
|
||||
@@ -0,0 +1,568 @@
|
||||
// undici's Dispatcher (lib/dispatcher/dispatcher.js), DispatcherBase
|
||||
// (dispatcher-base.js) and the request() API's handler (lib/api/api-request.js).
|
||||
import { EventEmitter } from "node:events";
|
||||
import { BodyReadable } from "./readable.ts";
|
||||
import {
|
||||
ClientClosedError,
|
||||
ClientDestroyedError,
|
||||
InvalidArgumentError,
|
||||
RequestAbortedError,
|
||||
ResponseStatusCodeError,
|
||||
} from "./errors.ts";
|
||||
import { parseHeaders, rawHeaderStrings } from "./util.ts";
|
||||
import type {
|
||||
DispatchController,
|
||||
DispatchFn,
|
||||
DispatchHandlers,
|
||||
DispatchInterceptor,
|
||||
DispatchOptions,
|
||||
IncomingHttpHeaders,
|
||||
Profile,
|
||||
} from "./types.ts";
|
||||
|
||||
/** Dispatcher.RequestOptions: the dispatch options (which carry the
|
||||
* request members, see DispatchOptions). */
|
||||
export type RequestOptions = DispatchOptions;
|
||||
|
||||
export interface ResponseData {
|
||||
statusCode: number;
|
||||
/** 7.x only. */
|
||||
statusText?: string;
|
||||
headers: IncomingHttpHeaders;
|
||||
body: BodyReadable;
|
||||
trailers: Record<string, string>;
|
||||
opaque: unknown;
|
||||
context: object | undefined;
|
||||
}
|
||||
|
||||
const defaultProfile: Profile = { major: 7 };
|
||||
|
||||
export class Dispatcher extends EventEmitter {
|
||||
dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return Promise.reject(new Error("not implemented"));
|
||||
}
|
||||
|
||||
destroy(_err?: Error | null): Promise<void> {
|
||||
return Promise.reject(new Error("not implemented"));
|
||||
}
|
||||
|
||||
/** Dispatcher.prototype.request: dispatches through this dispatcher and
|
||||
* resolves with the response once its headers arrive. */
|
||||
request(options: RequestOptions): Promise<ResponseData> {
|
||||
return requestThrough(this, options, this.__undiciProfile());
|
||||
}
|
||||
|
||||
__undiciProfile(): Profile {
|
||||
return defaultProfile;
|
||||
}
|
||||
|
||||
// Pool statistics hooks (undici's kConnected/kPending/kRunning/kSize/
|
||||
// kBusy); dispatchers that are not clients or pools count nothing.
|
||||
__connectedFlag(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
__isDestroyed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
__connectedCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__freeCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__pendingCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__queuedCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__runningCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__sizeCount(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class DispatcherBase extends Dispatcher {
|
||||
__profile: Profile;
|
||||
__destroyed: boolean;
|
||||
__closed: boolean;
|
||||
__onDestroyed: (() => void)[] | null;
|
||||
__onClosed: (() => void)[] | null;
|
||||
__interceptors: readonly DispatchInterceptor[];
|
||||
__interceptedDispatch: DispatchFn | null;
|
||||
|
||||
constructor(profile: Profile) {
|
||||
super();
|
||||
this.__profile = profile;
|
||||
this.__destroyed = false;
|
||||
this.__closed = false;
|
||||
this.__onDestroyed = [];
|
||||
this.__onClosed = [];
|
||||
this.__interceptors = [];
|
||||
this.__interceptedDispatch = null;
|
||||
}
|
||||
|
||||
__undiciProfile(): Profile {
|
||||
return this.__profile;
|
||||
}
|
||||
|
||||
get destroyed(): boolean {
|
||||
return this.__destroyed;
|
||||
}
|
||||
|
||||
__isDestroyed(): boolean {
|
||||
return this.__destroyed;
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return this.__closed;
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.__destroyed) {
|
||||
queueMicrotask(() => reject(new ClientDestroyedError()));
|
||||
return;
|
||||
}
|
||||
if (this.__closed) {
|
||||
const waiting = this.__onClosed;
|
||||
if (waiting !== null) waiting.push(() => resolve());
|
||||
else queueMicrotask(() => resolve());
|
||||
return;
|
||||
}
|
||||
this.__closed = true;
|
||||
this.__onClosed!.push(() => resolve());
|
||||
this.__close()
|
||||
.then(() => this.destroy())
|
||||
.then(() => {
|
||||
queueMicrotask(() => {
|
||||
const callbacks = this.__onClosed;
|
||||
this.__onClosed = null;
|
||||
if (callbacks !== null) for (const cb of callbacks) cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy(err?: Error | null): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.__destroyed) {
|
||||
const waiting = this.__onDestroyed;
|
||||
if (waiting !== null) waiting.push(() => resolve());
|
||||
else queueMicrotask(() => resolve());
|
||||
return;
|
||||
}
|
||||
const error = err ?? new ClientDestroyedError();
|
||||
this.__destroyed = true;
|
||||
this.__onDestroyed!.push(() => resolve());
|
||||
this.__destroy(error).then(() => {
|
||||
queueMicrotask(() => {
|
||||
const callbacks = this.__onDestroyed;
|
||||
this.__onDestroyed = null;
|
||||
if (callbacks !== null) for (const cb of callbacks) cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(options: DispatchOptions, handler: DispatchHandlers): boolean {
|
||||
const target = this.__profile.major >= 7 ? unwrapHandler(handler, this.__profile) : handler;
|
||||
try {
|
||||
if (this.__destroyed) throw new ClientDestroyedError();
|
||||
if (this.__closed) throw new ClientClosedError();
|
||||
return this.__dispatchIntercepted(options, target);
|
||||
} catch (err) {
|
||||
if (target.onError === undefined) {
|
||||
if (this.__profile.major >= 7) throw err;
|
||||
throw new InvalidArgumentError("invalid onError method");
|
||||
}
|
||||
target.onError!(err as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
__dispatchIntercepted(options: DispatchOptions, handler: DispatchHandlers): boolean {
|
||||
let dispatch = this.__interceptedDispatch;
|
||||
if (dispatch === null) {
|
||||
dispatch = (o: DispatchOptions, h: DispatchHandlers): boolean => this.__dispatch(o, h);
|
||||
for (let i = this.__interceptors.length - 1; i >= 0; i--) {
|
||||
dispatch = this.__interceptors[i]!(dispatch);
|
||||
}
|
||||
this.__interceptedDispatch = dispatch;
|
||||
}
|
||||
return dispatch(options, handler);
|
||||
}
|
||||
|
||||
__dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
__close(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
__destroy(_err: Error): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** The request() API over a dispatcher (api-request.js's request with the
|
||||
* promise callback). */
|
||||
export function requestThrough(
|
||||
dispatcher: Dispatcher,
|
||||
options: RequestOptions,
|
||||
profile: Profile,
|
||||
): Promise<ResponseData> {
|
||||
return new Promise<ResponseData>((resolve, reject) => {
|
||||
const callback = (err: Error | null, data: ResponseData | null): void => {
|
||||
if (err !== null) reject(err);
|
||||
else resolve(data!);
|
||||
};
|
||||
try {
|
||||
const handler = createRequestHandler(options, profile, callback);
|
||||
dispatcher.dispatch(options, handler);
|
||||
} catch (err) {
|
||||
queueMicrotask(() => callback(err as Error, null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** api-request.js's RequestHandler, as a handler record over closure state. */
|
||||
function createRequestHandler(
|
||||
opts: RequestOptions,
|
||||
profile: Profile,
|
||||
done: (err: Error | null, data: ResponseData | null) => void,
|
||||
): DispatchHandlers {
|
||||
if (opts.method === "CONNECT") throw new InvalidArgumentError("invalid method");
|
||||
if (
|
||||
opts.highWaterMark !== undefined &&
|
||||
(!Number.isFinite(opts.highWaterMark) || opts.highWaterMark < 0)
|
||||
) {
|
||||
throw new InvalidArgumentError("invalid highWaterMark");
|
||||
}
|
||||
const v7 = profile.major >= 7;
|
||||
const opaque: unknown = opts.opaque ?? null;
|
||||
const onInfo = opts.onInfo ?? null;
|
||||
const throwOnError = opts.throwOnError === true;
|
||||
const trailers: Record<string, string> = {};
|
||||
let callback: ((err: Error | null, data: ResponseData | null) => void) | null = done;
|
||||
let res: BodyReadable | null = null;
|
||||
let abortFn: ((err?: Error) => void) | null = null;
|
||||
let reason: Error | null = null;
|
||||
let removeAbortListener: (() => void) | null = null;
|
||||
|
||||
const removeSignal = (): void => {
|
||||
const remove = removeAbortListener;
|
||||
removeAbortListener = null;
|
||||
if (remove !== null) remove();
|
||||
};
|
||||
|
||||
const onError = (err: Error): void => {
|
||||
removeSignal();
|
||||
const cb = callback;
|
||||
if (cb !== null) {
|
||||
callback = null;
|
||||
queueMicrotask(() => cb(err, null));
|
||||
}
|
||||
const body = res;
|
||||
if (body !== null) {
|
||||
res = null;
|
||||
queueMicrotask(() => {
|
||||
body.on("error", () => {});
|
||||
body.destroy(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 5.x: abort() before the request reached a socket fails it at once.
|
||||
const abortV5 = (): void => {
|
||||
const abort = abortFn;
|
||||
if (abort !== null) abort();
|
||||
else onError(new RequestAbortedError());
|
||||
};
|
||||
|
||||
const signal = opts.signal ?? null;
|
||||
if (signal !== null) {
|
||||
if (v7) {
|
||||
if (signal.aborted) {
|
||||
reason = abortReason(signal);
|
||||
} else {
|
||||
const listener = (): void => {
|
||||
const why = abortReason(signal);
|
||||
reason = why;
|
||||
const body = res;
|
||||
const abort = abortFn;
|
||||
if (body !== null) {
|
||||
body.on("error", () => {});
|
||||
body.destroy(why);
|
||||
} else if (abort !== null) {
|
||||
abort(why);
|
||||
}
|
||||
};
|
||||
signal.addEventListener("abort", listener);
|
||||
removeAbortListener = () => signal.removeEventListener("abort", listener);
|
||||
}
|
||||
} else if (signal.aborted) {
|
||||
abortV5();
|
||||
} else {
|
||||
const listener = (): void => abortV5();
|
||||
signal.addEventListener("abort", listener);
|
||||
removeAbortListener = () => signal.removeEventListener("abort", listener);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onConnect: (abort: (err?: Error) => void): void => {
|
||||
if (v7) {
|
||||
if (reason !== null) {
|
||||
abort(reason);
|
||||
return;
|
||||
}
|
||||
} else if (callback === null) {
|
||||
throw new RequestAbortedError();
|
||||
}
|
||||
abortFn = abort;
|
||||
},
|
||||
onHeaders: (
|
||||
statusCode: number,
|
||||
rawHeaders: Buffer[],
|
||||
resume: () => void,
|
||||
statusText: string,
|
||||
): boolean => {
|
||||
const headers = parseHeaders(rawHeaderStrings(rawHeaders, profile), profile);
|
||||
if (statusCode < 200) {
|
||||
if (onInfo !== null) onInfo({ statusCode, headers });
|
||||
return true;
|
||||
}
|
||||
const contentTypeValue = headers["content-type"];
|
||||
const contentType = typeof contentTypeValue === "string" ? contentTypeValue : "";
|
||||
const abort = abortFn;
|
||||
const body = new BodyReadable(
|
||||
resume,
|
||||
(err: Error) => {
|
||||
if (abort !== null) {
|
||||
if (v7) abort(err);
|
||||
else abort();
|
||||
}
|
||||
},
|
||||
contentType,
|
||||
profile,
|
||||
);
|
||||
if (v7 && removeAbortListener !== null) body.on("close", () => removeSignal());
|
||||
const cb = callback;
|
||||
callback = null;
|
||||
res = body;
|
||||
if (cb !== null) {
|
||||
if (!v7 && throwOnError && statusCode >= 400) {
|
||||
resolveErrorBody(cb, body, contentType, statusCode, statusText, headers);
|
||||
return true;
|
||||
}
|
||||
const data: ResponseData = v7
|
||||
? { statusCode, statusText, headers, trailers, opaque, body, context: undefined }
|
||||
: { statusCode, headers, trailers, opaque, body, context: undefined };
|
||||
cb(null, data);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onData: (chunk: Buffer): boolean => {
|
||||
const body = res;
|
||||
return body !== null ? body.push(chunk) : true;
|
||||
},
|
||||
onComplete: (rawTrailers: string[] | null): void => {
|
||||
if (!v7) removeSignal();
|
||||
if (rawTrailers !== null) {
|
||||
const parsed = parseHeaders(rawTrailers, profile);
|
||||
for (const key of Object.keys(parsed)) {
|
||||
const value = parsed[key];
|
||||
if (typeof value === "string") trailers[key] = value;
|
||||
else if (value !== undefined) trailers[key] = value.join(", ");
|
||||
}
|
||||
}
|
||||
const body = res;
|
||||
if (body !== null) body.push(null);
|
||||
},
|
||||
onError,
|
||||
};
|
||||
}
|
||||
|
||||
function abortReason(signal: AbortSignal): Error {
|
||||
const reason: unknown = signal.reason;
|
||||
return reason instanceof Error ? reason : new RequestAbortedError();
|
||||
}
|
||||
|
||||
/** 5.x throwOnError: reject with ResponseStatusCodeError carrying the parsed
|
||||
* JSON or text body. */
|
||||
function resolveErrorBody(
|
||||
callback: (err: Error | null, data: ResponseData | null) => void,
|
||||
body: BodyReadable,
|
||||
contentType: string,
|
||||
statusCode: number,
|
||||
statusMessage: string,
|
||||
headers: IncomingHttpHeaders,
|
||||
): void {
|
||||
const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ""}`;
|
||||
const record: Record<string, string | string[]> = {};
|
||||
for (const key of Object.keys(headers)) {
|
||||
const value = headers[key];
|
||||
if (value !== undefined) record[key] = value;
|
||||
}
|
||||
if (statusCode === 204 || contentType === "") {
|
||||
body.dump();
|
||||
process.nextTick(() =>
|
||||
callback(new ResponseStatusCodeError(message, statusCode, record), null),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const finish = (payload: unknown): void => {
|
||||
process.nextTick(() =>
|
||||
callback(new ResponseStatusCodeError(message, statusCode, record, payload), null),
|
||||
);
|
||||
};
|
||||
const fallback = (): void => {
|
||||
body.dump();
|
||||
process.nextTick(() =>
|
||||
callback(new ResponseStatusCodeError(message, statusCode, record), null),
|
||||
);
|
||||
};
|
||||
if (contentType.startsWith("application/json")) {
|
||||
body.json().then((payload: unknown) => finish(payload), fallback);
|
||||
} else if (contentType.startsWith("text/")) {
|
||||
body.text().then((payload: string) => finish(payload), fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
|
||||
/** 7.x's UnwrapHandler: a controller-API handler (onRequestStart, ...)
|
||||
* driven through the legacy callbacks the clients invoke. */
|
||||
function unwrapHandler(handler: DispatchHandlers, profile: Profile): DispatchHandlers {
|
||||
if (handler.onRequestStart === undefined) return handler;
|
||||
let controller: UnwrapController | null = null;
|
||||
const controllerOf = (): UnwrapController => {
|
||||
let c = controller;
|
||||
if (c === null) {
|
||||
c = new UnwrapController(() => {});
|
||||
controller = c;
|
||||
}
|
||||
return c;
|
||||
};
|
||||
return {
|
||||
onConnect: (abort: (err?: Error) => void): void => {
|
||||
const c = new UnwrapController(abort);
|
||||
controller = c;
|
||||
if (handler.onRequestStart !== undefined) handler.onRequestStart!(c, undefined);
|
||||
},
|
||||
onHeaders: (
|
||||
statusCode: number,
|
||||
rawHeaders: Buffer[],
|
||||
resume: () => void,
|
||||
statusText: string,
|
||||
): boolean => {
|
||||
const c = controllerOf();
|
||||
c.__resume = resume;
|
||||
if (handler.onResponseStart !== undefined) {
|
||||
handler.onResponseStart!(
|
||||
c,
|
||||
statusCode,
|
||||
parseHeaders(rawHeaderStrings(rawHeaders, profile), profile),
|
||||
statusText,
|
||||
);
|
||||
}
|
||||
return !c.paused;
|
||||
},
|
||||
onData: (chunk: Buffer): boolean => {
|
||||
const c = controllerOf();
|
||||
if (handler.onResponseData !== undefined) handler.onResponseData!(c, chunk);
|
||||
return !c.paused;
|
||||
},
|
||||
onComplete: (trailers: string[] | null): void => {
|
||||
if (handler.onResponseEnd !== undefined) {
|
||||
handler.onResponseEnd!(controllerOf(), parseHeaders(trailers ?? [], profile));
|
||||
}
|
||||
},
|
||||
onError: (err: Error): void => {
|
||||
if (handler.onResponseError === undefined)
|
||||
throw new InvalidArgumentError("invalid onError method");
|
||||
handler.onResponseError!(controllerOf(), err);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 7.x's UnwrapController. */
|
||||
class UnwrapController implements DispatchController {
|
||||
__paused: boolean;
|
||||
__reason: Error | null;
|
||||
__aborted: boolean;
|
||||
__abort: (err?: Error) => void;
|
||||
__resume: (() => void) | null;
|
||||
|
||||
constructor(abort: (err?: Error) => void) {
|
||||
this.__paused = false;
|
||||
this.__reason = null;
|
||||
this.__aborted = false;
|
||||
this.__abort = abort;
|
||||
this.__resume = null;
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.__paused = true;
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (this.__paused) {
|
||||
this.__paused = false;
|
||||
const resume = this.__resume;
|
||||
if (resume !== null) resume();
|
||||
}
|
||||
}
|
||||
|
||||
abort(reason: Error): void {
|
||||
if (!this.__aborted) {
|
||||
this.__aborted = true;
|
||||
this.__reason = reason;
|
||||
this.__abort(reason);
|
||||
}
|
||||
}
|
||||
|
||||
get aborted(): boolean {
|
||||
return this.__aborted;
|
||||
}
|
||||
|
||||
get reason(): Error | null {
|
||||
return this.__reason;
|
||||
}
|
||||
|
||||
get paused(): boolean {
|
||||
return this.__paused;
|
||||
}
|
||||
}
|
||||
|
||||
/** The `Dispatcher.*` type namespace of undici's types/dispatcher.d.ts
|
||||
* (5.x and 7.x spellings). */
|
||||
export declare namespace Dispatcher {
|
||||
type DispatchOptions = import("./types.ts").DispatchOptions;
|
||||
type DispatchHandlers = import("./types.ts").DispatchHandlers;
|
||||
type DispatchHandler = import("./types.ts").DispatchHandlers;
|
||||
type DispatchController = import("./types.ts").DispatchController;
|
||||
type DispatchInterceptor = import("./types.ts").DispatchInterceptor;
|
||||
type Dispatch = import("./types.ts").DispatchFn;
|
||||
type HttpMethod = string;
|
||||
type UndiciHeaders = import("./types.ts").UndiciHeaders;
|
||||
// The 7.x spellings take an opaque type parameter; the opaque value is
|
||||
// `unknown` here.
|
||||
type RequestOptions<_TOpaque = null> = import("./dispatcher.ts").RequestOptions;
|
||||
type ResponseData<_TOpaque = null> = import("./dispatcher.ts").ResponseData;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// undici 5.x's `errors` export (lib/core/errors.js module.exports).
|
||||
export {
|
||||
HTTPParserError,
|
||||
UndiciError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
BodyTimeoutError,
|
||||
RequestContentLengthMismatchError,
|
||||
ConnectTimeoutError,
|
||||
ResponseStatusCodeError,
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError,
|
||||
ClientDestroyedError,
|
||||
ClientClosedError,
|
||||
InformationalError,
|
||||
SocketError,
|
||||
NotSupportedError,
|
||||
ResponseContentLengthMismatchError,
|
||||
BalancedPoolMissingUpstreamError,
|
||||
ResponseExceededMaxSizeError,
|
||||
} from "./errors.ts";
|
||||
@@ -0,0 +1,25 @@
|
||||
// undici 7.x's `errors` export (the implemented classes of
|
||||
// lib/core/errors.js module.exports).
|
||||
export {
|
||||
AbortError,
|
||||
HTTPParserError,
|
||||
UndiciError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
BodyTimeoutError,
|
||||
RequestContentLengthMismatchError,
|
||||
ConnectTimeoutError,
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError,
|
||||
ClientDestroyedError,
|
||||
ClientClosedError,
|
||||
InformationalError,
|
||||
SocketError,
|
||||
NotSupportedError,
|
||||
ResponseContentLengthMismatchError,
|
||||
BalancedPoolMissingUpstreamError,
|
||||
ResponseExceededMaxSizeError,
|
||||
RequestRetryError,
|
||||
ResponseError,
|
||||
} from "./errors.ts";
|
||||
@@ -0,0 +1,255 @@
|
||||
// undici's error classes (lib/core/errors.js), shared by the 5.x and 7.x
|
||||
// entries. Names, messages and codes are undici's; the 7.x-only classes are
|
||||
// exported by the 7.x entry alone.
|
||||
|
||||
export interface SocketInfo {
|
||||
localAddress?: string;
|
||||
localPort?: number;
|
||||
remoteAddress?: string;
|
||||
remotePort?: number;
|
||||
remoteFamily?: string;
|
||||
timeout?: number;
|
||||
bytesWritten?: number;
|
||||
bytesRead?: number;
|
||||
}
|
||||
|
||||
export class UndiciError extends Error {
|
||||
code: string;
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "UndiciError";
|
||||
this.code = "UND_ERR";
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectTimeoutError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "ConnectTimeoutError";
|
||||
this.message = message || "Connect Timeout Error";
|
||||
this.code = "UND_ERR_CONNECT_TIMEOUT";
|
||||
}
|
||||
}
|
||||
|
||||
export class HeadersTimeoutError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "HeadersTimeoutError";
|
||||
this.message = message || "Headers Timeout Error";
|
||||
this.code = "UND_ERR_HEADERS_TIMEOUT";
|
||||
}
|
||||
}
|
||||
|
||||
export class HeadersOverflowError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "HeadersOverflowError";
|
||||
this.message = message || "Headers Overflow Error";
|
||||
this.code = "UND_ERR_HEADERS_OVERFLOW";
|
||||
}
|
||||
}
|
||||
|
||||
export class BodyTimeoutError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "BodyTimeoutError";
|
||||
this.message = message || "Body Timeout Error";
|
||||
this.code = "UND_ERR_BODY_TIMEOUT";
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidArgumentError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "InvalidArgumentError";
|
||||
this.message = message || "Invalid Argument Error";
|
||||
this.code = "UND_ERR_INVALID_ARG";
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidReturnValueError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "InvalidReturnValueError";
|
||||
this.message = message || "Invalid Return Value Error";
|
||||
this.code = "UND_ERR_INVALID_RETURN_VALUE";
|
||||
}
|
||||
}
|
||||
|
||||
/** 7.x's base of RequestAbortedError (5.x has no AbortError export; its
|
||||
* RequestAbortedError extends UndiciError directly, which no 5.x program can
|
||||
* observe through the exported classes). */
|
||||
export class AbortError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "AbortError";
|
||||
this.message = message || "The operation was aborted";
|
||||
this.code = "UND_ERR_ABORT";
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestAbortedError extends AbortError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "AbortError";
|
||||
this.message = message || "Request aborted";
|
||||
this.code = "UND_ERR_ABORTED";
|
||||
}
|
||||
}
|
||||
|
||||
export class InformationalError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "InformationalError";
|
||||
this.message = message || "Request information";
|
||||
this.code = "UND_ERR_INFO";
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestContentLengthMismatchError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "RequestContentLengthMismatchError";
|
||||
this.message = message || "Request body length does not match content-length header";
|
||||
this.code = "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH";
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseContentLengthMismatchError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "ResponseContentLengthMismatchError";
|
||||
this.message = message || "Response body length does not match content-length header";
|
||||
this.code = "UND_ERR_RES_CONTENT_LENGTH_MISMATCH";
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientDestroyedError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "ClientDestroyedError";
|
||||
this.message = message || "The client is destroyed";
|
||||
this.code = "UND_ERR_DESTROYED";
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientClosedError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "ClientClosedError";
|
||||
this.message = message || "The client is closed";
|
||||
this.code = "UND_ERR_CLOSED";
|
||||
}
|
||||
}
|
||||
|
||||
export class SocketError extends UndiciError {
|
||||
socket: SocketInfo | null;
|
||||
constructor(message?: string, socket?: SocketInfo | null) {
|
||||
super(message);
|
||||
this.name = "SocketError";
|
||||
this.message = message || "Socket error";
|
||||
this.code = "UND_ERR_SOCKET";
|
||||
this.socket = socket ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotSupportedError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "NotSupportedError";
|
||||
this.message = message || "Not supported error";
|
||||
this.code = "UND_ERR_NOT_SUPPORTED";
|
||||
}
|
||||
}
|
||||
|
||||
export class BalancedPoolMissingUpstreamError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "MissingUpstreamError";
|
||||
this.message = message || "No upstream has been added to the BalancedPool";
|
||||
this.code = "UND_ERR_BPL_MISSING_UPSTREAM";
|
||||
}
|
||||
}
|
||||
|
||||
export class HTTPParserError extends Error {
|
||||
code: string | undefined;
|
||||
data: string | undefined;
|
||||
constructor(message?: string, code?: string, data?: string) {
|
||||
super(message);
|
||||
this.name = "HTTPParserError";
|
||||
this.code = code ? `HPE_${code}` : undefined;
|
||||
this.data = data ? data : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseExceededMaxSizeError extends UndiciError {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "ResponseExceededMaxSizeError";
|
||||
this.message = message || "Response content exceeded max size";
|
||||
this.code = "UND_ERR_RES_EXCEEDED_MAX_SIZE";
|
||||
}
|
||||
}
|
||||
|
||||
/** 5.x only (throwOnError). */
|
||||
export class ResponseStatusCodeError extends UndiciError {
|
||||
body: unknown;
|
||||
status: number;
|
||||
statusCode: number;
|
||||
headers: Record<string, string | string[]> | null;
|
||||
constructor(
|
||||
message?: string,
|
||||
statusCode?: number,
|
||||
headers?: Record<string, string | string[]> | null,
|
||||
body?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ResponseStatusCodeError";
|
||||
this.message = message || "Response Status Code Error";
|
||||
this.code = "UND_ERR_RESPONSE_STATUS_CODE";
|
||||
this.body = body;
|
||||
this.status = statusCode ?? 0;
|
||||
this.statusCode = statusCode ?? 0;
|
||||
this.headers = headers ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 7.x only. */
|
||||
export class RequestRetryError extends UndiciError {
|
||||
statusCode: number;
|
||||
data: { count: number };
|
||||
headers: Record<string, string | string[]>;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]>; data?: { count: number } },
|
||||
) {
|
||||
super(message);
|
||||
this.name = "RequestRetryError";
|
||||
this.message = message || "Request retry error";
|
||||
this.code = "UND_ERR_REQ_RETRY";
|
||||
this.statusCode = code;
|
||||
this.data = options.data ?? { count: 0 };
|
||||
this.headers = options.headers ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 7.x only. */
|
||||
export class ResponseError extends UndiciError {
|
||||
statusCode: number;
|
||||
body: unknown;
|
||||
headers: Record<string, string | string[]> | null;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]> | null; body?: unknown },
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ResponseError";
|
||||
this.message = message || "Response error";
|
||||
this.code = "UND_ERR_RESPONSE";
|
||||
this.statusCode = code;
|
||||
this.body = options.body;
|
||||
this.headers = options.headers ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// undici's fetch(). Without a dispatcher (no init.dispatcher and no
|
||||
// program-installed global dispatcher) it is the runtime's own fetch, which
|
||||
// implements the same WHATWG algorithm. With one, the request goes through
|
||||
// that dispatcher (its connector, lookup and pool), following undici's
|
||||
// fetch for request headers, redirects and errors; the response body is
|
||||
// buffered into a native Response.
|
||||
//
|
||||
// Known gaps: a Response built here reports `url` as "" and `redirected` as
|
||||
// false (the runtime's Response has no settable URL yet), and https requests
|
||||
// offer `gzip, deflate` rather than undici's `br, gzip, deflate` (the runtime
|
||||
// has no Brotli decoder).
|
||||
import { gunzipSync, inflateSync } from "node:zlib";
|
||||
import type { Dispatcher } from "./dispatcher.ts";
|
||||
import { getGlobalDispatcher, hasCustomGlobalDispatcher } from "./api.ts";
|
||||
import type { DispatchHandlers, Profile } from "./types.ts";
|
||||
|
||||
/** undici's RequestInit, restricted to the implemented members: string and
|
||||
* byte bodies, and the init members the dispatcher path honors. */
|
||||
export interface UndiciRequestInit {
|
||||
method?: string;
|
||||
headers?: Headers | [string, string][] | Record<string, string>;
|
||||
body?: string | Uint8Array | null;
|
||||
signal?: AbortSignal | null;
|
||||
redirect?: "follow" | "manual" | "error";
|
||||
dispatcher?: Dispatcher;
|
||||
}
|
||||
|
||||
/** The fetch standard's blocked ports (undici lib/web/fetch/constants.js). */
|
||||
const badPorts = new Set([
|
||||
"1",
|
||||
"7",
|
||||
"9",
|
||||
"11",
|
||||
"13",
|
||||
"15",
|
||||
"17",
|
||||
"19",
|
||||
"20",
|
||||
"21",
|
||||
"22",
|
||||
"23",
|
||||
"25",
|
||||
"37",
|
||||
"42",
|
||||
"43",
|
||||
"53",
|
||||
"69",
|
||||
"77",
|
||||
"79",
|
||||
"87",
|
||||
"95",
|
||||
"101",
|
||||
"102",
|
||||
"103",
|
||||
"104",
|
||||
"109",
|
||||
"110",
|
||||
"111",
|
||||
"113",
|
||||
"115",
|
||||
"117",
|
||||
"119",
|
||||
"123",
|
||||
"135",
|
||||
"137",
|
||||
"139",
|
||||
"143",
|
||||
"161",
|
||||
"179",
|
||||
"389",
|
||||
"427",
|
||||
"465",
|
||||
"512",
|
||||
"513",
|
||||
"514",
|
||||
"515",
|
||||
"526",
|
||||
"530",
|
||||
"531",
|
||||
"532",
|
||||
"540",
|
||||
"548",
|
||||
"554",
|
||||
"556",
|
||||
"563",
|
||||
"587",
|
||||
"601",
|
||||
"636",
|
||||
"989",
|
||||
"990",
|
||||
"993",
|
||||
"995",
|
||||
"1719",
|
||||
"1720",
|
||||
"1723",
|
||||
"2049",
|
||||
"3659",
|
||||
"4045",
|
||||
"4190",
|
||||
"5060",
|
||||
"5061",
|
||||
"6000",
|
||||
"6566",
|
||||
"6665",
|
||||
"6666",
|
||||
"6667",
|
||||
"6668",
|
||||
"6669",
|
||||
"6679",
|
||||
"6697",
|
||||
"10080",
|
||||
]);
|
||||
|
||||
interface RawResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: string[];
|
||||
body: Buffer;
|
||||
}
|
||||
|
||||
const redirectStatuses = [301, 302, 303, 307, 308];
|
||||
const nullBodyStatuses = [101, 204, 205, 304];
|
||||
|
||||
export function undiciFetch(
|
||||
input: string | URL,
|
||||
init: UndiciRequestInit | undefined,
|
||||
createGlobal: () => Dispatcher,
|
||||
_profile: Profile,
|
||||
): Promise<Response> {
|
||||
const explicit = init !== undefined ? init.dispatcher : undefined;
|
||||
if (explicit === undefined && !hasCustomGlobalDispatcher()) {
|
||||
if (init === undefined) return fetch(input);
|
||||
return fetch(input, {
|
||||
method: init.method,
|
||||
headers: init.headers,
|
||||
body: init.body,
|
||||
signal: init.signal ?? undefined,
|
||||
redirect: init.redirect,
|
||||
});
|
||||
}
|
||||
const dispatcher = explicit ?? getGlobalDispatcher(createGlobal);
|
||||
return fetchThrough(dispatcher, input, init ?? {});
|
||||
}
|
||||
|
||||
function normalizeMethod(method: string): string {
|
||||
const upper = method.toUpperCase();
|
||||
return ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"].includes(upper)
|
||||
? upper
|
||||
: method;
|
||||
}
|
||||
|
||||
async function fetchThrough(
|
||||
dispatcher: Dispatcher,
|
||||
input: string | URL,
|
||||
init: UndiciRequestInit,
|
||||
): Promise<Response> {
|
||||
let url = typeof input === "string" ? new URL(input) : new URL(input.href);
|
||||
let method = normalizeMethod(init.method ?? "GET");
|
||||
const signal = init.signal ?? null;
|
||||
const redirectMode = init.redirect ?? "follow";
|
||||
|
||||
// Request headers: the caller's (as a Headers list: lower-cased, sorted,
|
||||
// combined), then undici fetch's defaults.
|
||||
const headers = headerListOf(init.headers);
|
||||
let body: Buffer | null = null;
|
||||
const rawBody = init.body ?? null;
|
||||
if (rawBody !== null) {
|
||||
if (typeof rawBody === "string") {
|
||||
body = Buffer.from(rawBody);
|
||||
if (!headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
|
||||
} else {
|
||||
body = Buffer.from(rawBody.buffer, rawBody.byteOffset, rawBody.byteLength);
|
||||
}
|
||||
}
|
||||
if (!headers.has("accept")) headers.set("accept", "*/*");
|
||||
if (!headers.has("accept-language")) headers.set("accept-language", "*");
|
||||
|
||||
for (let redirects = 0; ; redirects++) {
|
||||
if (signal !== null && signal.aborted) throw abortReason(signal);
|
||||
if (badPorts.has(url.port)) {
|
||||
throw new TypeError("fetch failed", { cause: new Error("bad port") });
|
||||
}
|
||||
const requestHeaders = new HeaderList(headers);
|
||||
if (body !== null) {
|
||||
if (!requestHeaders.has("content-length"))
|
||||
requestHeaders.set("content-length", String(body.length));
|
||||
} else if ((method === "POST" || method === "PUT") && !requestHeaders.has("content-length")) {
|
||||
requestHeaders.set("content-length", "0");
|
||||
}
|
||||
requestHeaders.set("sec-fetch-mode", "cors");
|
||||
if (!requestHeaders.has("user-agent")) requestHeaders.set("user-agent", "undici");
|
||||
// undici also offers br over https; the native runtime has no Brotli
|
||||
// decoder, so only the codings it can decode are offered.
|
||||
if (!requestHeaders.has("accept-encoding"))
|
||||
requestHeaders.set("accept-encoding", "gzip, deflate");
|
||||
const pairs = requestHeaders.pairs();
|
||||
|
||||
let raw: RawResponse;
|
||||
try {
|
||||
raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal);
|
||||
} catch (err) {
|
||||
if (signal !== null && signal.aborted) throw abortReason(signal);
|
||||
throw new TypeError("fetch failed", { cause: err });
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
for (let i = 0; i + 1 < raw.headers.length; i += 2) {
|
||||
responseHeaders.append(raw.headers[i]!, raw.headers[i + 1]!);
|
||||
}
|
||||
const location = responseHeaders.get("location");
|
||||
if (redirectStatuses.includes(raw.status) && location !== null && redirectMode !== "manual") {
|
||||
if (redirectMode === "error") {
|
||||
throw new TypeError("fetch failed", { cause: new Error("unexpected redirect") });
|
||||
}
|
||||
if (redirects >= 20) {
|
||||
throw new TypeError("fetch failed", { cause: new Error("redirect count exceeded") });
|
||||
}
|
||||
const next = new URL(location, url);
|
||||
if (
|
||||
raw.status === 303 && method !== "GET" && method !== "HEAD"
|
||||
? true
|
||||
: (raw.status === 301 || raw.status === 302) && method === "POST"
|
||||
) {
|
||||
method = "GET";
|
||||
body = null;
|
||||
for (const name of [
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-location",
|
||||
"content-type",
|
||||
"content-length",
|
||||
]) {
|
||||
headers.delete(name);
|
||||
}
|
||||
}
|
||||
if (next.origin !== url.origin) {
|
||||
headers.delete("authorization");
|
||||
headers.delete("proxy-authorization");
|
||||
headers.delete("cookie");
|
||||
headers.delete("host");
|
||||
}
|
||||
url = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nullBody = nullBodyStatuses.includes(raw.status) || method === "HEAD";
|
||||
return new Response(
|
||||
nullBody ? null : decode(raw.body, responseHeaders.get("content-encoding")),
|
||||
{
|
||||
status: raw.status,
|
||||
statusText: raw.statusText,
|
||||
headers: responseHeaders,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** A Fetch header list: lower-cased names, values combined with ", " in
|
||||
* insertion order, serialized sorted by name (set-cookie values stay
|
||||
* separate) — what a Headers object yields when iterated. */
|
||||
class HeaderList {
|
||||
names: string[];
|
||||
values: Map<string, string[]>;
|
||||
|
||||
constructor(from: HeaderList | null) {
|
||||
this.names = from !== null ? from.names.slice() : [];
|
||||
this.values = new Map<string, string[]>();
|
||||
if (from !== null)
|
||||
for (const name of from.names) this.values.set(name, from.values.get(name)!.slice());
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.values.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
append(name: string, value: string): void {
|
||||
const key = name.toLowerCase();
|
||||
const normalized = value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
||||
const existing = this.values.get(key);
|
||||
if (existing !== undefined) existing.push(normalized);
|
||||
else {
|
||||
this.names.push(key);
|
||||
this.values.set(key, [normalized]);
|
||||
}
|
||||
}
|
||||
|
||||
set(name: string, value: string): void {
|
||||
this.delete(name);
|
||||
this.append(name, value);
|
||||
}
|
||||
|
||||
delete(name: string): void {
|
||||
const key = name.toLowerCase();
|
||||
if (!this.values.has(key)) return;
|
||||
this.values.delete(key);
|
||||
this.names = this.names.filter((n) => n !== key);
|
||||
}
|
||||
|
||||
pairs(): string[] {
|
||||
const out: string[] = [];
|
||||
for (const name of this.names.slice().sort()) {
|
||||
const list = this.values.get(name)!;
|
||||
if (name === "set-cookie") for (const value of list) out.push(name, value);
|
||||
else out.push(name, list.join(", "));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/** The caller's headers init as a header list. Records and pair arrays read
|
||||
* directly; a Headers object is read through forEach. */
|
||||
function headerListOf(
|
||||
init: Headers | [string, string][] | Record<string, string> | undefined,
|
||||
): HeaderList {
|
||||
const list = new HeaderList(null);
|
||||
if (init === undefined) return list;
|
||||
if (init instanceof Headers) {
|
||||
init.forEach((value: string, name: string) => list.append(name, value));
|
||||
} else if (Array.isArray(init)) {
|
||||
for (const pair of init) list.append(pair[0], pair[1]);
|
||||
} else {
|
||||
for (const name of Object.keys(init)) list.append(name, init[name]!);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function decode(body: Buffer, encoding: string | null): Buffer {
|
||||
if (encoding === null || body.length === 0) return body;
|
||||
let out = body;
|
||||
const codings = encoding
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.reverse();
|
||||
for (const coding of codings) {
|
||||
if (coding === "gzip" || coding === "x-gzip") out = gunzipSync(out);
|
||||
else if (coding === "deflate") out = inflateSync(out);
|
||||
else return body;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function abortReason(signal: AbortSignal): Error {
|
||||
const reason: unknown = signal.reason;
|
||||
return reason instanceof Error
|
||||
? reason
|
||||
: new DOMException("This operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
function dispatchOnce(
|
||||
dispatcher: Dispatcher,
|
||||
url: URL,
|
||||
method: string,
|
||||
headers: string[],
|
||||
body: Buffer | null,
|
||||
signal: AbortSignal | null,
|
||||
): Promise<RawResponse> {
|
||||
return new Promise<RawResponse>((resolve, reject) => {
|
||||
let status = 0;
|
||||
let statusText = "";
|
||||
let rawHeaders: string[] = [];
|
||||
const chunks: Buffer[] = [];
|
||||
let settled = false;
|
||||
let abortFn: ((err?: Error) => void) | null = null;
|
||||
const onAbort = (): void => {
|
||||
const abort = abortFn;
|
||||
if (abort !== null && signal !== null) abort(abortReason(signal));
|
||||
};
|
||||
if (signal !== null) signal.addEventListener("abort", onAbort);
|
||||
const finish = (err: Error | null): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (signal !== null) signal.removeEventListener("abort", onAbort);
|
||||
if (err !== null) reject(err);
|
||||
else resolve({ status, statusText, headers: rawHeaders, body: Buffer.concat(chunks) });
|
||||
};
|
||||
const handler: DispatchHandlers = {
|
||||
onConnect: (abort: (err?: Error) => void): void => {
|
||||
abortFn = abort;
|
||||
if (signal !== null && signal.aborted) abort(abortReason(signal));
|
||||
},
|
||||
onHeaders: (
|
||||
statusCode: number,
|
||||
raw: Buffer[],
|
||||
_resume: () => void,
|
||||
text: string,
|
||||
): boolean => {
|
||||
if (statusCode < 200) return true;
|
||||
status = statusCode;
|
||||
statusText = text;
|
||||
rawHeaders = [];
|
||||
for (const b of raw) rawHeaders.push(b.toString("latin1"));
|
||||
return true;
|
||||
},
|
||||
onData: (chunk: Buffer): boolean => {
|
||||
chunks.push(chunk);
|
||||
return true;
|
||||
},
|
||||
onComplete: (_trailers: string[] | null): void => finish(null),
|
||||
onError: (err: Error): void => finish(err),
|
||||
};
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
origin: url.origin,
|
||||
path: url.pathname + url.search,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
},
|
||||
handler,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// undici's Pool (lib/dispatcher/pool.js, pool-base.js, pool-stats.js): a
|
||||
// set of Clients for one origin, created on demand up to `connections`, with
|
||||
// a queue for requests no client can take.
|
||||
import { buildConnector } from "./connector.ts";
|
||||
import { Client } from "./client.ts";
|
||||
import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
|
||||
import { InvalidArgumentError } from "./errors.ts";
|
||||
import { parseOrigin } from "./util.ts";
|
||||
import type {
|
||||
ClientOptions,
|
||||
DispatchHandlers,
|
||||
DispatchOptions,
|
||||
PoolOptions,
|
||||
Profile,
|
||||
} from "./types.ts";
|
||||
|
||||
interface QueuedDispatch {
|
||||
opts: DispatchOptions;
|
||||
handler: DispatchHandlers;
|
||||
}
|
||||
|
||||
/** pool-stats.js: live counters over a pool's clients. */
|
||||
export class PoolStats {
|
||||
__pool: PoolBase;
|
||||
|
||||
constructor(pool: PoolBase) {
|
||||
this.__pool = pool;
|
||||
}
|
||||
|
||||
get connected(): number {
|
||||
return this.__pool.__connectedCount();
|
||||
}
|
||||
|
||||
get free(): number {
|
||||
return this.__pool.__freeCount();
|
||||
}
|
||||
|
||||
get pending(): number {
|
||||
return this.__pool.__pendingCount();
|
||||
}
|
||||
|
||||
get queued(): number {
|
||||
return this.__pool.__queuedCount();
|
||||
}
|
||||
|
||||
get running(): number {
|
||||
return this.__pool.__runningCount();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.__pool.__sizeCount();
|
||||
}
|
||||
}
|
||||
|
||||
export class PoolBase extends DispatcherBase {
|
||||
__clients: Dispatcher[];
|
||||
__clientNeedDrain: Map<Dispatcher, boolean>;
|
||||
__queue: QueuedDispatch[];
|
||||
__queued: number;
|
||||
__needDrain: boolean;
|
||||
__closedResolve: (() => void) | null;
|
||||
__stats: PoolStats;
|
||||
__onDrain: (origin: URL, targets: Dispatcher[]) => void;
|
||||
__onConnect: (origin: URL, targets: Dispatcher[]) => void;
|
||||
__onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
|
||||
__onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
|
||||
|
||||
constructor(profile: Profile) {
|
||||
super(profile);
|
||||
this.__clients = [];
|
||||
this.__clientNeedDrain = new Map<Dispatcher, boolean>();
|
||||
this.__queue = [];
|
||||
this.__queued = 0;
|
||||
this.__needDrain = false;
|
||||
this.__closedResolve = null;
|
||||
this.__stats = new PoolStats(this);
|
||||
this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
|
||||
const client = targets[0]!;
|
||||
let needDrain = false;
|
||||
while (!needDrain) {
|
||||
const item = this.__queue.shift();
|
||||
if (item === undefined) break;
|
||||
this.__queued--;
|
||||
needDrain = !client.dispatch(item.opts, item.handler);
|
||||
}
|
||||
this.__clientNeedDrain.set(client, needDrain);
|
||||
if (!needDrain && this.__needDrain) {
|
||||
this.__needDrain = false;
|
||||
this.emit("drain", origin, [this as Dispatcher, ...targets]);
|
||||
}
|
||||
const closed = this.__closedResolve;
|
||||
if (closed !== null && this.__queue.length === 0) {
|
||||
Promise.all(this.__clients.map((c) => c.close())).then(() => closed());
|
||||
}
|
||||
};
|
||||
this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
|
||||
this.emit("connect", origin, [this as Dispatcher, ...targets]);
|
||||
};
|
||||
this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
|
||||
this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
|
||||
};
|
||||
this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
|
||||
this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
|
||||
};
|
||||
}
|
||||
|
||||
get stats(): PoolStats {
|
||||
return this.__stats;
|
||||
}
|
||||
|
||||
__busyFlag(): boolean {
|
||||
return this.__needDrain;
|
||||
}
|
||||
|
||||
__connectedCount(): number {
|
||||
return this.__clients.filter((c) => c.__connectedFlag()).length;
|
||||
}
|
||||
|
||||
__freeCount(): number {
|
||||
return this.__clients.filter((c) => c.__connectedFlag() && !this.__clientNeedDrain.get(c))
|
||||
.length;
|
||||
}
|
||||
|
||||
__pendingCount(): number {
|
||||
let ret = this.__queued;
|
||||
for (const c of this.__clients) ret += c.__pendingCount();
|
||||
return ret;
|
||||
}
|
||||
|
||||
__queuedCount(): number {
|
||||
return this.__queued;
|
||||
}
|
||||
|
||||
__runningCount(): number {
|
||||
let ret = 0;
|
||||
for (const c of this.__clients) ret += c.__runningCount();
|
||||
return ret;
|
||||
}
|
||||
|
||||
__sizeCount(): number {
|
||||
let ret = this.__queued;
|
||||
for (const c of this.__clients) ret += c.__sizeCount();
|
||||
return ret;
|
||||
}
|
||||
|
||||
__close(): Promise<void> {
|
||||
if (this.__queue.length === 0) {
|
||||
return Promise.all(this.__clients.map((c) => c.close())).then(() => {});
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
this.__closedResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
__destroy(err: Error): Promise<void> {
|
||||
for (;;) {
|
||||
const item = this.__queue.shift();
|
||||
if (item === undefined) break;
|
||||
if (item.handler.onError !== undefined) item.handler.onError!(err);
|
||||
}
|
||||
return Promise.all(this.__clients.map((c) => c.destroy(err))).then(() => {});
|
||||
}
|
||||
|
||||
__dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
|
||||
const dispatcher = this.__getDispatcher();
|
||||
if (dispatcher === null) {
|
||||
this.__needDrain = true;
|
||||
this.__queue.push({ opts, handler });
|
||||
this.__queued++;
|
||||
} else if (!dispatcher.dispatch(opts, handler)) {
|
||||
this.__clientNeedDrain.set(dispatcher, true);
|
||||
this.__needDrain = this.__getDispatcher() === null;
|
||||
}
|
||||
return !this.__needDrain;
|
||||
}
|
||||
|
||||
__getDispatcher(): Dispatcher | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
__addClient(client: Dispatcher): void {
|
||||
client.on("drain", this.__onDrain);
|
||||
client.on("connect", this.__onConnect);
|
||||
client.on("disconnect", this.__onDisconnect);
|
||||
client.on("connectionError", this.__onConnectionError);
|
||||
this.__clients.push(client);
|
||||
this.__clientNeedDrain.set(client, false);
|
||||
if (this.__needDrain) {
|
||||
process.nextTick(() => {
|
||||
if (this.__needDrain) this.__onDrain(clientUrl(client), [client, this as Dispatcher]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clientUrl(client: Dispatcher): URL {
|
||||
return client instanceof Client ? client.__url : new URL("http://localhost");
|
||||
}
|
||||
|
||||
export class Pool extends PoolBase {
|
||||
__connections: number | null;
|
||||
__url: URL;
|
||||
__options: ClientOptions;
|
||||
__factory: ((origin: URL, opts: object) => Dispatcher) | null;
|
||||
|
||||
constructor(origin: string | URL, options: PoolOptions | undefined, profile: Profile) {
|
||||
super(profile);
|
||||
const opts: PoolOptions = options ?? {};
|
||||
const connections = opts.connections ?? null;
|
||||
if (connections !== null && (!Number.isFinite(connections) || connections < 0)) {
|
||||
throw new InvalidArgumentError("invalid connections");
|
||||
}
|
||||
const connect = opts.connect ?? null;
|
||||
const connector =
|
||||
typeof connect === "function"
|
||||
? connect
|
||||
: buildConnector(
|
||||
connect !== null
|
||||
? {
|
||||
...connect,
|
||||
timeout:
|
||||
connect.timeout !== undefined ? connect.timeout : (opts.connectTimeout ?? 10e3),
|
||||
}
|
||||
: { timeout: opts.connectTimeout ?? 10e3 },
|
||||
);
|
||||
const interceptors = opts.interceptors;
|
||||
if (profile.major < 7 && interceptors !== undefined && interceptors.Pool !== undefined) {
|
||||
this.__interceptors = interceptors.Pool;
|
||||
}
|
||||
this.__connections = connections !== null && connections !== 0 ? connections : null;
|
||||
this.__url = parseOrigin(origin, profile);
|
||||
this.__options = {
|
||||
keepAliveTimeout: opts.keepAliveTimeout,
|
||||
keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
|
||||
keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
|
||||
pipelining: opts.pipelining,
|
||||
maxHeaderSize: opts.maxHeaderSize,
|
||||
bodyTimeout: opts.bodyTimeout,
|
||||
headersTimeout: opts.headersTimeout,
|
||||
strictContentLength: opts.strictContentLength,
|
||||
maxRequestsPerClient: opts.maxRequestsPerClient,
|
||||
maxResponseSize: opts.maxResponseSize,
|
||||
localAddress: opts.localAddress,
|
||||
interceptors: interceptors !== undefined ? { Client: interceptors.Client } : undefined,
|
||||
connect: connector,
|
||||
};
|
||||
const factory = opts.factory;
|
||||
this.__factory = factory !== undefined ? factory : null;
|
||||
if (profile.major >= 7) {
|
||||
// 7.x drops a client whose connection attempt failed.
|
||||
this.on("connectionError", (_origin: URL, targets: Dispatcher[], _err: Error) => {
|
||||
for (const target of targets) {
|
||||
const idx = this.__clients.indexOf(target);
|
||||
if (idx !== -1) this.__clients.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
__getDispatcher(): Dispatcher | null {
|
||||
for (const client of this.__clients) {
|
||||
if (!this.__clientNeedDrain.get(client)) return client;
|
||||
}
|
||||
if (this.__connections === null || this.__clients.length < this.__connections) {
|
||||
const factory = this.__factory;
|
||||
const client =
|
||||
factory !== null
|
||||
? factory(this.__url, this.__options)
|
||||
: this.__createClient(this.__url, this.__options);
|
||||
this.__addClient(client);
|
||||
return client;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The default factory: a Client of the same undici version. */
|
||||
__createClient(origin: URL, options: ClientOptions): Dispatcher {
|
||||
return new Client(origin, options, this.__profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// undici's response body (lib/api/readable.js): a readable byte stream fed
|
||||
// by the dispatch handler, with the fetch body mixin (text/json/
|
||||
// arrayBuffer/blob/bytes) and dump().
|
||||
//
|
||||
// It follows node:stream's Readable contract for the members it declares
|
||||
// (flowing on a 'data' listener or resume(), pause(), 'data'/'end'/'close'/
|
||||
// 'error' events and their ordering, destroy(err), back-pressure through
|
||||
// push()'s return value), built on EventEmitter: scriptc compiles program
|
||||
// classes extending EventEmitter under both its own Node declarations and
|
||||
// @types/node, which a node:stream subclass does not get.
|
||||
import { EventEmitter } from "node:events";
|
||||
import { NotSupportedError, RequestAbortedError } from "./errors.ts";
|
||||
import type { Profile } from "./types.ts";
|
||||
|
||||
const HIGH_WATER_MARK = 64 * 1024;
|
||||
|
||||
export class BodyReadable extends EventEmitter {
|
||||
__resumeFn: () => void;
|
||||
__abortFn: (err: Error) => void;
|
||||
__contentType: string;
|
||||
__consuming: boolean;
|
||||
__profile: Profile;
|
||||
__buffer: Buffer[];
|
||||
__length: number;
|
||||
__flowing: boolean;
|
||||
__ended: boolean;
|
||||
__endEmitted: boolean;
|
||||
__destroyed: boolean;
|
||||
__flowScheduled: boolean;
|
||||
__needsResume: boolean;
|
||||
|
||||
constructor(
|
||||
resume: () => void,
|
||||
abort: (err: Error) => void,
|
||||
contentType: string,
|
||||
profile: Profile,
|
||||
) {
|
||||
super();
|
||||
this.__resumeFn = resume;
|
||||
this.__abortFn = abort;
|
||||
this.__contentType = contentType;
|
||||
this.__consuming = false;
|
||||
this.__profile = profile;
|
||||
this.__buffer = [];
|
||||
this.__length = 0;
|
||||
this.__flowing = false;
|
||||
this.__ended = false;
|
||||
this.__endEmitted = false;
|
||||
this.__destroyed = false;
|
||||
this.__flowScheduled = false;
|
||||
this.__needsResume = false;
|
||||
// Readable.on('data') switches the stream to flowing mode.
|
||||
this.on("newListener", (event: string) => {
|
||||
if (event === "data" && !this.__flowing) this.resume();
|
||||
});
|
||||
}
|
||||
|
||||
get destroyed(): boolean {
|
||||
return this.__destroyed;
|
||||
}
|
||||
|
||||
get readableEnded(): boolean {
|
||||
return this.__endEmitted;
|
||||
}
|
||||
|
||||
get readableLength(): number {
|
||||
return this.__length;
|
||||
}
|
||||
|
||||
get readableFlowing(): boolean {
|
||||
return this.__flowing;
|
||||
}
|
||||
|
||||
/** Readable.push: queue a chunk (null ends the body). Returns false once
|
||||
* the buffer reaches the high-water mark; the dispatcher then pauses the
|
||||
* connection until the consumer drains it. */
|
||||
push(chunk: Buffer | null): boolean {
|
||||
if (this.__destroyed || this.__ended) return false;
|
||||
if (chunk === null) {
|
||||
this.__ended = true;
|
||||
if (this.__length === 0) this.__scheduleEnd();
|
||||
else this.__scheduleFlow();
|
||||
return false;
|
||||
}
|
||||
if (this.__flowing && this.__length === 0 && this.listenerCount("data") > 0) {
|
||||
this.emit("data", chunk);
|
||||
return true;
|
||||
}
|
||||
this.__buffer.push(chunk);
|
||||
this.__length += chunk.length;
|
||||
if (this.__flowing) this.__scheduleFlow();
|
||||
const below = this.__length < HIGH_WATER_MARK;
|
||||
if (!below) this.__needsResume = true;
|
||||
return below;
|
||||
}
|
||||
|
||||
resume(): this {
|
||||
if (!this.__flowing) {
|
||||
this.__flowing = true;
|
||||
this.__scheduleFlow();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
pause(): this {
|
||||
this.__flowing = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy(err?: Error): this {
|
||||
if (this.__destroyed) return this;
|
||||
this.__destroyed = true;
|
||||
let error: Error | null = err ?? null;
|
||||
if (error === null && !this.__endEmitted) error = new RequestAbortedError();
|
||||
if (error !== null) this.__abortFn(error);
|
||||
this.__buffer = [];
|
||||
this.__length = 0;
|
||||
const reported = error;
|
||||
process.nextTick(() => {
|
||||
if (reported !== null) this.emit("error", reported);
|
||||
this.emit("close");
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
__scheduleFlow(): void {
|
||||
if (this.__flowScheduled) return;
|
||||
this.__flowScheduled = true;
|
||||
process.nextTick(() => {
|
||||
this.__flowScheduled = false;
|
||||
this.__flow();
|
||||
});
|
||||
}
|
||||
|
||||
__flow(): void {
|
||||
while (this.__flowing && !this.__destroyed && this.__buffer.length > 0) {
|
||||
const chunk = this.__buffer.shift()!;
|
||||
this.__length -= chunk.length;
|
||||
this.emit("data", chunk);
|
||||
}
|
||||
if (this.__destroyed) return;
|
||||
if (this.__needsResume && this.__length < HIGH_WATER_MARK) {
|
||||
this.__needsResume = false;
|
||||
this.__resumeFn();
|
||||
}
|
||||
if (this.__ended && this.__length === 0 && this.__flowing) this.__scheduleEnd();
|
||||
}
|
||||
|
||||
__scheduleEnd(): void {
|
||||
process.nextTick(() => {
|
||||
if (this.__endEmitted || this.__destroyed || !this.__flowing || this.__length > 0) return;
|
||||
this.__endEmitted = true;
|
||||
this.emit("end");
|
||||
// autoDestroy: a fully read body closes.
|
||||
process.nextTick(() => {
|
||||
if (this.__destroyed) return;
|
||||
this.__destroyed = true;
|
||||
this.emit("close");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** True once text()/json()/arrayBuffer()/bytes()/blob()/dump() started
|
||||
* consuming the body. */
|
||||
get bodyUsed(): boolean {
|
||||
return this.__consuming;
|
||||
}
|
||||
|
||||
text(): Promise<string> {
|
||||
return this.__consume().then((bytes) => decodeText(bytes, this.__profile));
|
||||
}
|
||||
|
||||
// undici declares json() as Promise<any>: callers assign the parsed value
|
||||
// to their own annotated types without a cast, exactly as with undici.
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
json(): Promise<any> {
|
||||
return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile)));
|
||||
}
|
||||
|
||||
arrayBuffer(): Promise<ArrayBuffer> {
|
||||
return this.__consume().then((bytes) => {
|
||||
const out = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(out).set(bytes);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
bytes(): Promise<Uint8Array> {
|
||||
return this.__consume().then((bytes) => new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
blob(): Promise<Blob> {
|
||||
return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType }));
|
||||
}
|
||||
|
||||
formData(): Promise<never> {
|
||||
return Promise.reject(new NotSupportedError());
|
||||
}
|
||||
|
||||
/** Reads and discards up to `limit` bytes (default 256 KiB on 5.x, 128 KiB
|
||||
* on 7.x), then resolves; read errors are swallowed like undici's. */
|
||||
dump(opts?: { limit: number }): Promise<void> {
|
||||
let limit =
|
||||
opts !== undefined && Number.isFinite(opts.limit)
|
||||
? opts.limit
|
||||
: this.__profile.major >= 7
|
||||
? 131072
|
||||
: 262144;
|
||||
this.__consuming = true;
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.__endEmitted || this.__destroyed) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.on("data", (chunk: Buffer) => {
|
||||
limit -= chunk.length;
|
||||
if (limit < 0) {
|
||||
this.destroy();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
this.on("end", () => resolve());
|
||||
this.on("error", () => resolve());
|
||||
this.on("close", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
__consume(): Promise<Buffer> {
|
||||
if (this.bodyUsed) return Promise.reject(new TypeError("unusable"));
|
||||
this.__consuming = true;
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let done = false;
|
||||
this.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
this.on("end", () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
this.on("error", (err: Error) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
reject(err);
|
||||
});
|
||||
this.on("close", () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
reject(new RequestAbortedError());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 5.x decodes UTF-8 as-is; 7.x strips a leading byte-order mark first. */
|
||||
function decodeText(bytes: Buffer, profile: Profile): string {
|
||||
if (
|
||||
profile.major >= 7 &&
|
||||
bytes.length >= 3 &&
|
||||
bytes[0] === 0xef &&
|
||||
bytes[1] === 0xbb &&
|
||||
bytes[2] === 0xbf
|
||||
) {
|
||||
return bytes.subarray(3).toString("utf8");
|
||||
}
|
||||
return bytes.toString("utf8");
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// undici's per-dispatch request record (lib/core/request.js): option
|
||||
// validation, header processing, and the handler callbacks with their
|
||||
// diagnostics_channel publications.
|
||||
import { channels } from "./diagnostics.ts";
|
||||
import { InvalidArgumentError, NotSupportedError } from "./errors.ts";
|
||||
import type { DispatchHandlers, DispatchOptions, Profile } from "./types.ts";
|
||||
|
||||
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
|
||||
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
|
||||
const invalidPathRegex = /[^!-ÿ]/;
|
||||
|
||||
export class Request {
|
||||
origin: string;
|
||||
method: string;
|
||||
path: string;
|
||||
body: Buffer | null;
|
||||
/** Header name/value pairs in insertion order (host and content-length
|
||||
* are consumed into their own fields, like undici). */
|
||||
headerPairs: string[];
|
||||
host: string | null;
|
||||
contentLength: number | null;
|
||||
contentType: string | null;
|
||||
completed: boolean;
|
||||
aborted: boolean;
|
||||
idempotent: boolean;
|
||||
blocking: boolean;
|
||||
reset: boolean | null;
|
||||
headersTimeout: number | null;
|
||||
bodyTimeout: number | null;
|
||||
throwOnError: boolean;
|
||||
servername: string | null;
|
||||
error: Error | null;
|
||||
abortFn: ((err?: Error) => void) | null;
|
||||
handler: DispatchHandlers;
|
||||
/** The request as diagnostics_channel subscribers see it: one boxed
|
||||
* value per request, so its identity is stable across publications. */
|
||||
diagnostic: unknown;
|
||||
|
||||
constructor(origin: string, opts: DispatchOptions, handler: DispatchHandlers, profile: Profile) {
|
||||
const path = opts.path;
|
||||
const method = opts.method;
|
||||
if (typeof path !== "string") {
|
||||
throw new InvalidArgumentError("path must be a string");
|
||||
} else if (
|
||||
path[0] !== "/" &&
|
||||
!(path.startsWith("http://") || path.startsWith("https://")) &&
|
||||
method !== "CONNECT"
|
||||
) {
|
||||
throw new InvalidArgumentError("path must be an absolute URL or start with a slash");
|
||||
} else if (invalidPathRegex.test(path)) {
|
||||
throw new InvalidArgumentError("invalid request path");
|
||||
}
|
||||
if (typeof method !== "string") {
|
||||
throw new InvalidArgumentError("method must be a string");
|
||||
} else if (!tokenRegExp.test(method)) {
|
||||
throw new InvalidArgumentError("invalid request method");
|
||||
}
|
||||
if (method === "CONNECT") {
|
||||
throw new NotSupportedError(
|
||||
"scriptc's undici client does not implement CONNECT requests yet",
|
||||
);
|
||||
}
|
||||
const headersTimeout = opts.headersTimeout ?? null;
|
||||
if (headersTimeout !== null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
|
||||
throw new InvalidArgumentError("invalid headersTimeout");
|
||||
}
|
||||
const bodyTimeout = opts.bodyTimeout ?? null;
|
||||
if (bodyTimeout !== null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
|
||||
throw new InvalidArgumentError("invalid bodyTimeout");
|
||||
}
|
||||
|
||||
this.origin = origin;
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.headersTimeout = headersTimeout;
|
||||
this.bodyTimeout = bodyTimeout;
|
||||
// 7.x dropped throwOnError (the responseError interceptor replaced it).
|
||||
if (profile.major >= 7 && opts.throwOnError !== undefined) {
|
||||
throw new InvalidArgumentError("invalid throwOnError");
|
||||
}
|
||||
this.throwOnError = opts.throwOnError === true;
|
||||
|
||||
const body = opts.body ?? null;
|
||||
if (body === null) {
|
||||
this.body = null;
|
||||
} else if (typeof body === "string") {
|
||||
this.body = body.length > 0 ? Buffer.from(body) : null;
|
||||
} else {
|
||||
this.body =
|
||||
body.byteLength > 0 ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null;
|
||||
}
|
||||
|
||||
this.completed = false;
|
||||
this.aborted = false;
|
||||
this.idempotent = opts.idempotent ?? (method === "HEAD" || method === "GET");
|
||||
this.blocking = opts.blocking ?? (profile.major >= 7 ? method !== "HEAD" : false);
|
||||
this.reset = opts.reset ?? null;
|
||||
this.host = null;
|
||||
this.contentLength = null;
|
||||
this.contentType = null;
|
||||
this.headerPairs = [];
|
||||
this.error = null;
|
||||
this.abortFn = null;
|
||||
|
||||
const headers = opts.headers ?? null;
|
||||
if (headers !== null) {
|
||||
if (Array.isArray(headers)) {
|
||||
if (headers.length % 2 !== 0) throw new InvalidArgumentError("headers array must be even");
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
processHeader(this, headers[i]!, headers[i + 1]!, profile);
|
||||
}
|
||||
} else {
|
||||
for (const key of Object.keys(headers)) {
|
||||
const value = headers[key];
|
||||
if (value === undefined) continue;
|
||||
if (typeof value === "string") {
|
||||
processHeader(this, key, value, profile);
|
||||
} else if (profile.major >= 7) {
|
||||
for (const item of value) processHeader(this, key, item, profile);
|
||||
} else {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.servername = serverName(this.host);
|
||||
this.handler = handler;
|
||||
this.diagnostic = null;
|
||||
if (channels.create.hasSubscribers) channels.create.publish({ request: this.diagnosticView() });
|
||||
}
|
||||
|
||||
diagnosticView(): unknown {
|
||||
let view = this.diagnostic;
|
||||
if (view === null) {
|
||||
view = {
|
||||
origin: this.origin,
|
||||
completed: this.completed,
|
||||
method: this.method,
|
||||
path: this.path,
|
||||
headers: this.headers,
|
||||
};
|
||||
this.diagnostic = view;
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/** The DiagnosticsChannel.Request view's raw header block. */
|
||||
get headers(): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < this.headerPairs.length; i += 2) {
|
||||
out += `${this.headerPairs[i]}: ${this.headerPairs[i + 1]}\r\n`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
addHeader(key: string, value: string): Request {
|
||||
this.headerPairs.push(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
onBodySent(chunk: Buffer): void {
|
||||
const cb = this.handler.onBodySent;
|
||||
if (cb !== undefined) {
|
||||
try {
|
||||
this.handler.onBodySent!(chunk.length, chunk.length);
|
||||
} catch (err) {
|
||||
this.onError(err as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRequestSent(): void {
|
||||
if (channels.bodySent.hasSubscribers)
|
||||
channels.bodySent.publish({ request: this.diagnosticView() });
|
||||
}
|
||||
|
||||
onConnect(abort: (err?: Error) => void): void {
|
||||
if (this.error !== null) {
|
||||
abort(this.error);
|
||||
return;
|
||||
}
|
||||
this.abortFn = abort;
|
||||
if (this.handler.onConnect !== undefined) this.handler.onConnect!(abort);
|
||||
}
|
||||
|
||||
onHeaders(
|
||||
statusCode: number,
|
||||
headers: Buffer[],
|
||||
resume: () => void,
|
||||
statusText: string,
|
||||
): boolean {
|
||||
if (channels.headers.hasSubscribers) {
|
||||
channels.headers.publish({
|
||||
request: this.diagnosticView(),
|
||||
response: { statusCode, headers, statusText },
|
||||
});
|
||||
}
|
||||
if (this.handler.onHeaders === undefined) return true;
|
||||
return this.handler.onHeaders!(statusCode, headers, resume, statusText);
|
||||
}
|
||||
|
||||
onData(chunk: Buffer): boolean {
|
||||
if (this.handler.onData === undefined) return true;
|
||||
return this.handler.onData!(chunk);
|
||||
}
|
||||
|
||||
onComplete(trailers: Buffer[]): void {
|
||||
this.completed = true;
|
||||
if (channels.trailers.hasSubscribers)
|
||||
channels.trailers.publish({ request: this.diagnosticView(), trailers });
|
||||
if (this.handler.onComplete !== undefined) {
|
||||
const raw: string[] = [];
|
||||
for (const t of trailers) raw.push(t.toString("latin1"));
|
||||
this.handler.onComplete!(raw);
|
||||
}
|
||||
}
|
||||
|
||||
onError(error: Error): void {
|
||||
if (channels.error.hasSubscribers)
|
||||
channels.error.publish({ request: this.diagnosticView(), error });
|
||||
if (this.aborted) return;
|
||||
this.aborted = true;
|
||||
if (this.handler.onError !== undefined) this.handler.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
function processHeader(request: Request, key: string, val: string, profile: Profile): void {
|
||||
const lower = key.toLowerCase();
|
||||
if (lower === "host") {
|
||||
if (request.host === null) {
|
||||
request.host = val;
|
||||
return;
|
||||
}
|
||||
if (profile.major >= 7) throw new InvalidArgumentError("duplicate host header");
|
||||
} else if (lower === "content-length") {
|
||||
if (request.contentLength === null) {
|
||||
const n = parseInt(val, 10);
|
||||
if (!Number.isFinite(n) || (profile.major >= 7 && !/^\d+$/.test(val))) {
|
||||
throw new InvalidArgumentError("invalid content-length header");
|
||||
}
|
||||
request.contentLength = n;
|
||||
return;
|
||||
}
|
||||
if (profile.major >= 7) throw new InvalidArgumentError("duplicate content-length header");
|
||||
}
|
||||
if (lower === "content-type" && request.contentType === null && !headerCharRegex.test(val)) {
|
||||
request.contentType = val;
|
||||
request.headerPairs.push(key, val);
|
||||
return;
|
||||
}
|
||||
if (lower === "transfer-encoding" || lower === "keep-alive" || lower === "upgrade") {
|
||||
throw new InvalidArgumentError(`invalid ${lower} header`);
|
||||
}
|
||||
if (lower === "connection") {
|
||||
if (profile.major < 7) throw new InvalidArgumentError("invalid connection header");
|
||||
for (const token of val.toLowerCase().split(",")) {
|
||||
const trimmed = token.trim();
|
||||
if (!tokenRegExp.test(trimmed)) throw new InvalidArgumentError("invalid connection header");
|
||||
if (trimmed === "close") request.reset = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lower === "expect") throw new NotSupportedError("expect header not supported");
|
||||
if (!tokenRegExp.test(key)) throw new InvalidArgumentError("invalid header key");
|
||||
if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
|
||||
request.headerPairs.push(key, val);
|
||||
}
|
||||
|
||||
/** util.getServerName: the Host header without its port; null for IPs. */
|
||||
function serverName(host: string | null): string | null {
|
||||
if (host === null || host === "") return null;
|
||||
let name = host;
|
||||
if (name.startsWith("[")) {
|
||||
const idx = name.indexOf("]");
|
||||
if (idx !== -1) name = name.slice(1, idx);
|
||||
return null;
|
||||
}
|
||||
const idx = name.indexOf(":");
|
||||
if (idx !== -1) name = name.slice(0, idx);
|
||||
return /^\d+\.\d+\.\d+\.\d+$/.test(name) ? null : name;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// undici 7.x's fast timers (lib/util/timers.js): one unref'd native timer
|
||||
// ticking every 499 ms drives every headers/body timeout, so those fire on
|
||||
// the first tick at least `delay` after the timer (re)started.
|
||||
const RESOLUTION_MS = 1e3;
|
||||
const TICK_MS = (RESOLUTION_MS >> 1) - 1;
|
||||
const NOT_IN_LIST = -2;
|
||||
const TO_BE_CLEARED = -1;
|
||||
const PENDING = 0;
|
||||
const ACTIVE = 1;
|
||||
|
||||
let fastNow = 0;
|
||||
// The ticking native timer, held as its refresh closure.
|
||||
let refreshTick: (() => void) | null = null;
|
||||
const fastTimers: FastTimer[] = [];
|
||||
|
||||
function onTick(): void {
|
||||
fastNow += TICK_MS;
|
||||
let idx = 0;
|
||||
let len = fastTimers.length;
|
||||
while (idx < len) {
|
||||
const timer = fastTimers[idx]!;
|
||||
if (timer.state === PENDING) {
|
||||
timer.idleStart = fastNow - TICK_MS;
|
||||
timer.state = ACTIVE;
|
||||
} else if (timer.state === ACTIVE && fastNow >= timer.idleStart + timer.idleTimeout) {
|
||||
timer.state = TO_BE_CLEARED;
|
||||
timer.idleStart = -1;
|
||||
timer.onTimeout();
|
||||
}
|
||||
if (timer.state === TO_BE_CLEARED) {
|
||||
timer.state = NOT_IN_LIST;
|
||||
if (--len !== 0) fastTimers[idx] = fastTimers[len]!;
|
||||
} else {
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
fastTimers.length = len;
|
||||
if (fastTimers.length !== 0) refreshTimeout();
|
||||
}
|
||||
|
||||
function refreshTimeout(): void {
|
||||
const refresh = refreshTick;
|
||||
if (refresh !== null) {
|
||||
refresh();
|
||||
} else {
|
||||
const timeout = setTimeout(onTick, TICK_MS);
|
||||
timeout.unref();
|
||||
refreshTick = () => {
|
||||
timeout.refresh();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class FastTimer {
|
||||
state: number;
|
||||
idleTimeout: number;
|
||||
idleStart: number;
|
||||
onTimeout: () => void;
|
||||
|
||||
constructor(callback: () => void, delay: number) {
|
||||
this.state = NOT_IN_LIST;
|
||||
this.idleTimeout = delay;
|
||||
this.idleStart = -1;
|
||||
this.onTimeout = callback;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (this.state === NOT_IN_LIST) fastTimers.push(this);
|
||||
if (refreshTick === null || fastTimers.length === 1) refreshTimeout();
|
||||
this.state = PENDING;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.state = TO_BE_CLEARED;
|
||||
this.idleStart = -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Types shared by the undici entries. They restate the subset of undici's
|
||||
// own declarations (types/dispatcher.d.ts, connector.d.ts, ...) that the
|
||||
// shim implements; test/package-shims/undici-types.test.ts checks them
|
||||
// against the installed 5.x and 7.x declarations.
|
||||
import type { Socket } from "node:net";
|
||||
import type { Dispatcher } from "./dispatcher.ts";
|
||||
|
||||
/** Response and request header records (undici's IncomingHttpHeaders). */
|
||||
export type IncomingHttpHeaders = Record<string, string | string[] | undefined>;
|
||||
|
||||
/** What a request's `headers` option accepts here: a record or a flat
|
||||
* [name, value, ...] array. */
|
||||
export type UndiciHeaders = Record<string, string | string[] | undefined> | string[] | null;
|
||||
|
||||
/** The payloads the shim writes. Streams, iterables, Blob and FormData
|
||||
* bodies are outside the implemented subset. */
|
||||
export type RequestBody = string | Buffer | Uint8Array | null;
|
||||
|
||||
export interface DispatchOptions {
|
||||
origin?: string | URL;
|
||||
path: string;
|
||||
method: string;
|
||||
body?: RequestBody;
|
||||
headers?: UndiciHeaders;
|
||||
idempotent?: boolean;
|
||||
blocking?: boolean;
|
||||
headersTimeout?: number | null;
|
||||
bodyTimeout?: number | null;
|
||||
reset?: boolean;
|
||||
throwOnError?: boolean;
|
||||
// Dispatcher.RequestOptions' members live here too: a record passed to
|
||||
// dispatch() is copied to this shape, and interceptors read them back
|
||||
// through a `params as Dispatcher.RequestOptions` cast, as with undici.
|
||||
opaque?: unknown;
|
||||
signal?: AbortSignal | null;
|
||||
onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
|
||||
highWaterMark?: number;
|
||||
}
|
||||
|
||||
/** undici's dispatch-controller (7.x handler API). */
|
||||
export interface DispatchController {
|
||||
readonly aborted: boolean;
|
||||
readonly paused: boolean;
|
||||
readonly reason: Error | null;
|
||||
abort(reason: Error): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
|
||||
/** Dispatch handlers: the 5.x callbacks, plus 7.x's controller callbacks. */
|
||||
export interface DispatchHandlers {
|
||||
onConnect?(abort: (err?: Error) => void): void;
|
||||
onError?(err: Error): void;
|
||||
onHeaders?(
|
||||
statusCode: number,
|
||||
headers: Buffer[],
|
||||
resume: () => void,
|
||||
statusText: string,
|
||||
): boolean;
|
||||
onData?(chunk: Buffer): boolean;
|
||||
onComplete?(trailers: string[] | null): void;
|
||||
onBodySent?(chunkSize: number, totalBytesSent: number): void;
|
||||
onRequestStart?(controller: DispatchController, context: unknown): void;
|
||||
onResponseStart?(
|
||||
controller: DispatchController,
|
||||
statusCode: number,
|
||||
headers: IncomingHttpHeaders,
|
||||
statusMessage?: string,
|
||||
): void;
|
||||
onResponseData?(controller: DispatchController, chunk: Buffer): void;
|
||||
onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void;
|
||||
onResponseError?(controller: DispatchController, error: Error): void;
|
||||
}
|
||||
|
||||
export type DispatchFn = (options: DispatchOptions, handler: DispatchHandlers) => boolean;
|
||||
|
||||
export type DispatchInterceptor = (dispatch: DispatchFn) => DispatchFn;
|
||||
|
||||
/** buildConnector's per-connection parameters. */
|
||||
export interface ConnectorOptions {
|
||||
hostname: string;
|
||||
host?: string;
|
||||
protocol: string;
|
||||
port: string;
|
||||
servername?: string;
|
||||
localAddress?: string | null;
|
||||
}
|
||||
|
||||
export type ConnectorCallback = (err: Error | null, socket: Socket | null) => void;
|
||||
|
||||
/** A connector: dials, reports through the callback, and returns the socket
|
||||
* (undici 5 and 7 both return it although their types declare void). */
|
||||
export type Connector = (options: ConnectorOptions, callback: ConnectorCallback) => Socket;
|
||||
|
||||
/** net/tls connect options a connector forwards (the implemented subset). */
|
||||
export interface ConnectorBuildOptions {
|
||||
timeout?: number | null;
|
||||
maxCachedSessions?: number | null;
|
||||
socketPath?: string | null;
|
||||
port?: number;
|
||||
rejectUnauthorized?: boolean;
|
||||
ca?: string | Buffer | (string | Buffer)[];
|
||||
servername?: string;
|
||||
/** net.connect's resolver hook: @types/node's LookupFunction is
|
||||
* assignable (method syntax; the callback takes one address or all). */
|
||||
lookup?(
|
||||
hostname: string,
|
||||
options: unknown,
|
||||
callback: (
|
||||
err: NodeJS.ErrnoException | null,
|
||||
address: string | LookupAddress[],
|
||||
family?: number,
|
||||
) => void,
|
||||
): void;
|
||||
keepAlive?: boolean;
|
||||
keepAliveInitialDelay?: number;
|
||||
noDelay?: boolean;
|
||||
autoSelectFamily?: boolean;
|
||||
autoSelectFamilyAttemptTimeout?: number;
|
||||
}
|
||||
|
||||
export interface LookupAddress {
|
||||
address: string;
|
||||
family: number;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
keepAliveTimeout?: number | null;
|
||||
keepAliveMaxTimeout?: number | null;
|
||||
keepAliveTimeoutThreshold?: number | null;
|
||||
pipelining?: number | null;
|
||||
connect?: ConnectorBuildOptions | Connector | null;
|
||||
maxHeaderSize?: number | null;
|
||||
bodyTimeout?: number | null;
|
||||
headersTimeout?: number | null;
|
||||
strictContentLength?: boolean;
|
||||
maxRequestsPerClient?: number;
|
||||
maxResponseSize?: number | null;
|
||||
connectTimeout?: number | null;
|
||||
localAddress?: string;
|
||||
/** 5.x applies `Client` interceptors; 7.x ignores the option. */
|
||||
interceptors?: { Client?: readonly DispatchInterceptor[] | undefined };
|
||||
}
|
||||
|
||||
export interface PoolOptions extends ClientOptions {
|
||||
factory?(origin: URL, opts: object): Dispatcher;
|
||||
connections?: number | null;
|
||||
interceptors?: {
|
||||
Pool?: readonly DispatchInterceptor[] | undefined;
|
||||
Client?: readonly DispatchInterceptor[] | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentOptions extends PoolOptions {
|
||||
factory?(origin: string | URL, opts: object): Dispatcher;
|
||||
interceptors?: {
|
||||
Agent?: readonly DispatchInterceptor[] | undefined;
|
||||
Pool?: readonly DispatchInterceptor[] | undefined;
|
||||
Client?: readonly DispatchInterceptor[] | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/** Version-dependent behavior, selected by the importing package's installed
|
||||
* undici (the 5.x or 7.x entry). */
|
||||
export interface Profile {
|
||||
major: number;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Helpers from undici's lib/core/util.js.
|
||||
import { InvalidArgumentError } from "./errors.ts";
|
||||
import type { IncomingHttpHeaders, Profile } from "./types.ts";
|
||||
|
||||
/** util.parseHeaders over raw [name, value, ...] pairs: lower-cased names,
|
||||
* repeated names collected into arrays. 5.x decodes values as UTF-8 and
|
||||
* treats an empty first value as absent; 7.x decodes latin1. */
|
||||
export function parseHeaders(raw: readonly string[], profile: Profile): IncomingHttpHeaders {
|
||||
const obj: IncomingHttpHeaders = {};
|
||||
for (let i = 0; i + 1 < raw.length; i += 2) {
|
||||
const key = raw[i]!.toLowerCase();
|
||||
const value = raw[i + 1]!;
|
||||
const existing = obj[key];
|
||||
const absent = profile.major >= 7 ? existing === undefined : !existing;
|
||||
if (absent) {
|
||||
obj[key] = value;
|
||||
} else if (typeof existing === "string") {
|
||||
obj[key] = [existing, value];
|
||||
} else if (existing !== undefined) {
|
||||
existing.push(value);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** Raw header Buffers as strings (latin1 on 7.x, UTF-8 on 5.x). */
|
||||
export function rawHeaderStrings(raw: readonly Buffer[], profile: Profile): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
// Names are ASCII tokens; values follow the version's decoding.
|
||||
out.push(
|
||||
i % 2 === 0 || profile.major >= 7 ? raw[i]!.toString("latin1") : raw[i]!.toString("utf8"),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** util.parseOrigin: an http(s) origin URL. */
|
||||
export function parseOrigin(url: string | URL, profile: Profile): URL {
|
||||
const parsed = parseURL(url, profile);
|
||||
if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
|
||||
throw new InvalidArgumentError("invalid url");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** util.parseURL for the string/URL forms. */
|
||||
export function parseURL(url: string | URL, profile: Profile): URL {
|
||||
const parsed = typeof url === "string" ? new URL(url) : url;
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
// 7.x checks only string inputs; 5.x checks every input.
|
||||
if (profile.major < 7) throw new InvalidArgumentError("invalid protocol");
|
||||
if (typeof url === "string") {
|
||||
throw new InvalidArgumentError(
|
||||
"Invalid URL protocol: the URL must start with `http:` or `https:`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** util.parseKeepAliveTimeout: the server's Keep-Alive `timeout=<s>` hint in
|
||||
* milliseconds, or null. */
|
||||
export function parseKeepAliveTimeout(value: string): number | null {
|
||||
const m = /timeout=(\d+)/.exec(value);
|
||||
return m !== null ? parseInt(m[1]!, 10) * 1000 : null;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// scriptc's built-in implementation of the `undici` package, 5.x API.
|
||||
//
|
||||
// Programs whose installed undici is 5.x compile this module in its place
|
||||
// (see packages/compiler/src/frontend/package-shims.ts). It implements the
|
||||
// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
|
||||
// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
|
||||
// the diagnostics_channel publications. Anything else undici exports is not
|
||||
// declared here, so using it is a compile-time error.
|
||||
import { Agent as CoreAgent } from "./core/agent.ts";
|
||||
import {
|
||||
getGlobalDispatcher as coreGetGlobalDispatcher,
|
||||
request as coreRequest,
|
||||
setGlobalDispatcher as coreSetGlobalDispatcher,
|
||||
} from "./core/api.ts";
|
||||
import type { TopLevelRequestOptions } from "./core/api.ts";
|
||||
import { Client as CoreClient } from "./core/client.ts";
|
||||
import { buildConnector as coreBuildConnector } from "./core/connector.ts";
|
||||
import { Dispatcher } from "./core/dispatcher.ts";
|
||||
import type { ResponseData } from "./core/dispatcher.ts";
|
||||
import { undiciFetch } from "./core/fetch.ts";
|
||||
import type { UndiciRequestInit } from "./core/fetch.ts";
|
||||
import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
|
||||
import type {
|
||||
AgentOptions,
|
||||
ClientOptions,
|
||||
Connector,
|
||||
ConnectorBuildOptions,
|
||||
ConnectorCallback,
|
||||
ConnectorOptions,
|
||||
DispatchOptions,
|
||||
PoolOptions,
|
||||
Profile,
|
||||
} from "./core/types.ts";
|
||||
import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
|
||||
|
||||
const profile: Profile = { major: 5 };
|
||||
|
||||
export { Dispatcher };
|
||||
export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
|
||||
export * as errors from "./core/errors-v5.ts";
|
||||
|
||||
export class Client extends CoreClient {
|
||||
constructor(url: string | URL, options?: Client.Options) {
|
||||
super(url, options, profile);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Client {
|
||||
type Options = ClientOptions;
|
||||
type SocketInfo = CoreSocketInfo;
|
||||
}
|
||||
|
||||
export class Pool extends CorePool {
|
||||
constructor(url: string | URL, options?: Pool.Options) {
|
||||
super(url, options, profile);
|
||||
}
|
||||
|
||||
__createClient(origin: URL, options: ClientOptions): Dispatcher {
|
||||
return new Client(origin, options);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Pool {
|
||||
type Options = PoolOptions;
|
||||
type PoolStats = CorePoolStats;
|
||||
}
|
||||
|
||||
export class Agent extends CoreAgent {
|
||||
constructor(options?: Agent.Options) {
|
||||
super(options, profile);
|
||||
}
|
||||
|
||||
__createClient(origin: string | URL, options: PoolOptions): Dispatcher {
|
||||
return new Client(origin, options);
|
||||
}
|
||||
|
||||
__createPool(origin: string | URL, options: PoolOptions): Dispatcher {
|
||||
return new Pool(origin, options);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Agent {
|
||||
type Options = AgentOptions;
|
||||
type DispatchOptions = import("./core/types.ts").DispatchOptions;
|
||||
}
|
||||
|
||||
export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
|
||||
return coreBuildConnector(options ?? {});
|
||||
}
|
||||
|
||||
export declare namespace buildConnector {
|
||||
type BuildOptions = ConnectorBuildOptions;
|
||||
type Options = ConnectorOptions;
|
||||
type Callback = ConnectorCallback;
|
||||
type connector = Connector;
|
||||
}
|
||||
|
||||
export function setGlobalDispatcher(dispatcher: Dispatcher): void {
|
||||
coreSetGlobalDispatcher(dispatcher);
|
||||
}
|
||||
|
||||
export function getGlobalDispatcher(): Dispatcher {
|
||||
return coreGetGlobalDispatcher(() => new Agent());
|
||||
}
|
||||
|
||||
export function request(
|
||||
url: string | URL,
|
||||
options?: TopLevelRequestOptions,
|
||||
): Promise<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
return undiciFetch(input, init, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
// undici's Headers is the WHATWG class the runtime implements natively.
|
||||
export import Headers = globalThis.Headers;
|
||||
export type { DispatchOptions };
|
||||
@@ -0,0 +1,119 @@
|
||||
// scriptc's built-in implementation of the `undici` package, 7.x API.
|
||||
//
|
||||
// Programs whose installed undici is 7.x compile this module in its place
|
||||
// (see packages/compiler/src/frontend/package-shims.ts). It implements the
|
||||
// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
|
||||
// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
|
||||
// the diagnostics_channel publications. Anything else undici exports is not
|
||||
// declared here, so using it is a compile-time error.
|
||||
import { Agent as CoreAgent } from "./core/agent.ts";
|
||||
import {
|
||||
getGlobalDispatcher as coreGetGlobalDispatcher,
|
||||
request as coreRequest,
|
||||
setGlobalDispatcher as coreSetGlobalDispatcher,
|
||||
} from "./core/api.ts";
|
||||
import type { TopLevelRequestOptions } from "./core/api.ts";
|
||||
import { Client as CoreClient } from "./core/client.ts";
|
||||
import { buildConnector as coreBuildConnector } from "./core/connector.ts";
|
||||
import { Dispatcher } from "./core/dispatcher.ts";
|
||||
import type { ResponseData } from "./core/dispatcher.ts";
|
||||
import { undiciFetch } from "./core/fetch.ts";
|
||||
import type { UndiciRequestInit } from "./core/fetch.ts";
|
||||
import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
|
||||
import type {
|
||||
AgentOptions,
|
||||
ClientOptions,
|
||||
Connector,
|
||||
ConnectorBuildOptions,
|
||||
ConnectorCallback,
|
||||
ConnectorOptions,
|
||||
DispatchOptions,
|
||||
PoolOptions,
|
||||
Profile,
|
||||
} from "./core/types.ts";
|
||||
import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
|
||||
|
||||
const profile: Profile = { major: 7 };
|
||||
|
||||
export { Dispatcher };
|
||||
export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
|
||||
export * as errors from "./core/errors-v7.ts";
|
||||
|
||||
export class Client extends CoreClient {
|
||||
constructor(url: string | URL, options?: Client.Options) {
|
||||
super(url, options, profile);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Client {
|
||||
type Options = ClientOptions;
|
||||
type SocketInfo = CoreSocketInfo;
|
||||
}
|
||||
|
||||
export class Pool extends CorePool {
|
||||
constructor(url: string | URL, options?: Pool.Options) {
|
||||
super(url, options, profile);
|
||||
}
|
||||
|
||||
__createClient(origin: URL, options: ClientOptions): Dispatcher {
|
||||
return new Client(origin, options);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Pool {
|
||||
type Options = PoolOptions;
|
||||
type PoolStats = CorePoolStats;
|
||||
}
|
||||
|
||||
export class Agent extends CoreAgent {
|
||||
constructor(options?: Agent.Options) {
|
||||
super(options, profile);
|
||||
}
|
||||
|
||||
__createClient(origin: string | URL, options: PoolOptions): Dispatcher {
|
||||
return new Client(origin, options);
|
||||
}
|
||||
|
||||
__createPool(origin: string | URL, options: PoolOptions): Dispatcher {
|
||||
return new Pool(origin, options);
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace Agent {
|
||||
type Options = AgentOptions;
|
||||
type DispatchOptions = import("./core/types.ts").DispatchOptions;
|
||||
}
|
||||
|
||||
export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
|
||||
return coreBuildConnector(options ?? {});
|
||||
}
|
||||
|
||||
export declare namespace buildConnector {
|
||||
type BuildOptions = ConnectorBuildOptions;
|
||||
type Options = ConnectorOptions;
|
||||
type Callback = ConnectorCallback;
|
||||
type connector = Connector;
|
||||
}
|
||||
|
||||
export function setGlobalDispatcher(dispatcher: Dispatcher): void {
|
||||
coreSetGlobalDispatcher(dispatcher);
|
||||
}
|
||||
|
||||
export function getGlobalDispatcher(): Dispatcher {
|
||||
return coreGetGlobalDispatcher(() => new Agent());
|
||||
}
|
||||
|
||||
export function request(
|
||||
url: string | URL,
|
||||
options?: TopLevelRequestOptions,
|
||||
): Promise<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
return undiciFetch(input, init, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
// undici's Headers is the WHATWG class the runtime implements natively.
|
||||
export import Headers = globalThis.Headers;
|
||||
export type { DispatchOptions };
|
||||
@@ -16,6 +16,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
fileHandle: features.fileHandle,
|
||||
fetch: features.fetch,
|
||||
...(features.eventTarget ? { eventTarget: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
zlib: features.zlib,
|
||||
assert: features.assert,
|
||||
inspect: features.inspect,
|
||||
@@ -40,5 +42,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
tls: features.tls,
|
||||
tlsCa: features.tlsCa,
|
||||
cipher: features.cipher,
|
||||
webcrypto: features.webcrypto,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function compilePackedLibrary(
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
cipher: false,
|
||||
webcrypto: false,
|
||||
tlsCa: false,
|
||||
};
|
||||
const selection = await loadRuntimePack({
|
||||
|
||||
@@ -3671,13 +3671,30 @@ export class LlEmitter {
|
||||
}
|
||||
defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``);
|
||||
}
|
||||
// Ordering also decides inline when the first bytes differ: byte order
|
||||
// is the plain comparison's order, and the UTF-16 order whenever both
|
||||
// bytes are ASCII. Callers only test the sign of the result.
|
||||
for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) {
|
||||
if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`)) continue;
|
||||
// Ordering decides inline from the first differing bytes. The bytes
|
||||
// before them are equal, so both strings are at the same position of a
|
||||
// character: either both bytes continue characters with the same lead
|
||||
// byte, or both start a character. Byte order is the UTF-16 order for
|
||||
// two continuation bytes, and for two lead bytes when one starts a
|
||||
// character below U+D000, whose UTF-16 unit is below every unit of a
|
||||
// character with a lead byte of at least 0xED. Between lead bytes of at
|
||||
// least 0xED, a supplementary character (0xF0..0xF4, a high surrogate
|
||||
// unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of
|
||||
// 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime.
|
||||
// Differing words without a byte of at least 0xED decide in byte
|
||||
// order; other differing words decide by their first differing byte,
|
||||
// and strings shorter than a word by their first byte. Otherwise a
|
||||
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
|
||||
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
|
||||
// strings, so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// only test the sign of the result.
|
||||
if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
|
||||
const fn = "scr_str_cmp_u16";
|
||||
this.declare("declare i64 @llvm.bswap.i64(i64)");
|
||||
const utf16 = fn === "scr_str_cmp_u16";
|
||||
this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)");
|
||||
this.declare("declare i8 @llvm.umin.i8(i8, i8)");
|
||||
this.declare("declare i32 @scr_str_cmp(ptr, ptr)");
|
||||
this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)");
|
||||
defs.push(
|
||||
`define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
@@ -3692,8 +3709,7 @@ export class LlEmitter {
|
||||
` %bempty = icmp eq ${this.sizeType} %blen, 0`,
|
||||
` %empty = or i1 %aempty, %bempty`,
|
||||
` br i1 %empty, label %order, label %words`,
|
||||
// Both at least 8 bytes: the first differing byte of one word pair.
|
||||
// Byte-swapped little-endian words compare in byte order.
|
||||
// Both at least 8 bytes: the first word of each.
|
||||
`words:`,
|
||||
` %along = icmp uge ${this.sizeType} %alen, 8`,
|
||||
` %blong = icmp uge ${this.sizeType} %blen, 8`,
|
||||
@@ -3705,22 +3721,7 @@ export class LlEmitter {
|
||||
` %aw = load i64, ptr %awp, align 1`,
|
||||
` %bw = load i64, ptr %bwp, align 1`,
|
||||
` %wdiffer = icmp ne i64 %aw, %bw`,
|
||||
` br i1 %wdiffer, label %wdiff, label %wsame`,
|
||||
`wdiff:`,
|
||||
...(utf16
|
||||
? [
|
||||
` %wbits = or i64 %aw, %bw`,
|
||||
` %whigh = and i64 %wbits, -9187201950435737472`,
|
||||
` %wascii = icmp eq i64 %whigh, 0`,
|
||||
` br i1 %wascii, label %wordorder, label %order`,
|
||||
]
|
||||
: [` br label %wordorder`]),
|
||||
`wordorder:`,
|
||||
` %asw = call i64 @llvm.bswap.i64(i64 %aw)`,
|
||||
` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`,
|
||||
` %wbelow = icmp ult i64 %asw, %bsw`,
|
||||
` %wsign = select i1 %wbelow, i32 -1, i32 1`,
|
||||
` ret i32 %wsign`,
|
||||
` br i1 %wdiffer, label %differ, label %wsame`,
|
||||
// Equal first words and one length of at most 16 bytes: the word
|
||||
// ending at that length covers the rest, and any difference lies
|
||||
// past the equal first word.
|
||||
@@ -3736,42 +3737,89 @@ export class LlEmitter {
|
||||
` %at = load i64, ptr %atp, align 1`,
|
||||
` %bt = load i64, ptr %btp, align 1`,
|
||||
` %tdiffer = icmp ne i64 %at, %bt`,
|
||||
` br i1 %tdiffer, label %tdiff, label %equal`,
|
||||
`tdiff:`,
|
||||
...(utf16
|
||||
? [
|
||||
` %tbits = or i64 %at, %bt`,
|
||||
` %thigh = and i64 %tbits, -9187201950435737472`,
|
||||
` %tascii = icmp eq i64 %thigh, 0`,
|
||||
` br i1 %tascii, label %tailorder, label %order`,
|
||||
]
|
||||
: [` br label %tailorder`]),
|
||||
`tailorder:`,
|
||||
` %ats = call i64 @llvm.bswap.i64(i64 %at)`,
|
||||
` %bts = call i64 @llvm.bswap.i64(i64 %bt)`,
|
||||
` %tbelow = icmp ult i64 %ats, %bts`,
|
||||
` %tsign = select i1 %tbelow, i32 -1, i32 1`,
|
||||
` ret i32 %tsign`,
|
||||
` br i1 %tdiffer, label %differ, label %equal`,
|
||||
`first:`,
|
||||
` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`,
|
||||
` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`,
|
||||
` %x = load i8, ptr %adata`,
|
||||
` %y = load i8, ptr %bdata`,
|
||||
` %differ = icmp ne i8 %x, %y`,
|
||||
...(utf16
|
||||
? [
|
||||
` %bits = or i8 %x, %y`,
|
||||
` %ascii = icmp sge i8 %bits, 0`,
|
||||
` %decide = and i1 %differ, %ascii`,
|
||||
]
|
||||
: [` %decide = and i1 %differ, true`]),
|
||||
` br i1 %decide, label %byte, label %order`,
|
||||
` %fdiffer = icmp ne i8 %x, %y`,
|
||||
` br i1 %fdiffer, label %bytes, label %order`,
|
||||
// Two differing words. When neither has a byte of at least 0xED
|
||||
// (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are
|
||||
// set), byte-swapped little-endian words compare in byte order.
|
||||
// Otherwise the lowest set byte of their xor is the first differing
|
||||
// byte.
|
||||
`differ:`,
|
||||
` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`,
|
||||
` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`,
|
||||
` %xl = and i64 %dx, 9187201950435737471`,
|
||||
` %yl = and i64 %dy, 9187201950435737471`,
|
||||
` %xa = add i64 %xl, 1374463283923456787`,
|
||||
` %ya = add i64 %yl, 1374463283923456787`,
|
||||
` %xh = and i64 %dx, %xa`,
|
||||
` %yh = and i64 %dy, %ya`,
|
||||
` %hh = or i64 %xh, %yh`,
|
||||
` %whigh = and i64 %hh, -9187201950435737472`,
|
||||
` %dlow = icmp eq i64 %whigh, 0`,
|
||||
` br i1 %dlow, label %wordorder, label %locate`,
|
||||
`wordorder:`,
|
||||
` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`,
|
||||
` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`,
|
||||
` %wbelow = icmp ult i64 %xsw, %ysw`,
|
||||
` %wsign = select i1 %wbelow, i32 -1, i32 1`,
|
||||
` ret i32 %wsign`,
|
||||
`locate:`,
|
||||
` %dxor = xor i64 %dx, %dy`,
|
||||
` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`,
|
||||
` %dshift = and i64 %dtz, 56`,
|
||||
` %dxs = lshr i64 %dx, %dshift`,
|
||||
` %dys = lshr i64 %dy, %dshift`,
|
||||
` %dxb = trunc i64 %dxs to i8`,
|
||||
` %dyb = trunc i64 %dys to i8`,
|
||||
` br label %bytes`,
|
||||
// The first differing bytes.
|
||||
`bytes:`,
|
||||
` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`,
|
||||
` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`,
|
||||
` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`,
|
||||
` %blow = icmp ult i8 %bmin, -19`,
|
||||
` br i1 %blow, label %byte, label %high`,
|
||||
// Both at least 0xED. A lone surrogate (0xED) takes the runtime;
|
||||
// supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF
|
||||
// once 0x10 is subtracted from them.
|
||||
`high:`,
|
||||
` %xed = icmp eq i8 %bx, -19`,
|
||||
` %yed = icmp eq i8 %by, -19`,
|
||||
` %ed = or i1 %xed, %yed`,
|
||||
` br i1 %ed, label %order, label %planes`,
|
||||
`planes:`,
|
||||
` %xsupp = icmp uge i8 %bx, -16`,
|
||||
` %ysupp = icmp uge i8 %by, -16`,
|
||||
` %xlow = add i8 %bx, -16`,
|
||||
` %ylow = add i8 %by, -16`,
|
||||
` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`,
|
||||
` %yk = select i1 %ysupp, i8 %ylow, i8 %by`,
|
||||
` %kbelow = icmp ult i8 %xk, %yk`,
|
||||
` %ksign = select i1 %kbelow, i32 -1, i32 1`,
|
||||
` ret i32 %ksign`,
|
||||
`byte:`,
|
||||
` %below = icmp ult i8 %x, %y`,
|
||||
` %below = icmp ult i8 %bx, %by`,
|
||||
` %sign = select i1 %below, i32 -1, i32 1`,
|
||||
` ret i32 %sign`,
|
||||
`order:`,
|
||||
` %c = call i32 @${fn}(ptr %a, ptr %b)`,
|
||||
` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`,
|
||||
` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`,
|
||||
` %acw = load i32, ptr %acw.p`,
|
||||
` %bcw = load i32, ptr %bcw.p`,
|
||||
` %cws = or i32 %acw, %bcw`,
|
||||
` %anyascii = icmp slt i32 %cws, 0`,
|
||||
` br i1 %anyascii, label %memorder, label %units`,
|
||||
`memorder:`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
|
||||
` ret i32 %m`,
|
||||
`units:`,
|
||||
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
` ret i32 %c`,
|
||||
`equal:`,
|
||||
` ret i32 0`,
|
||||
@@ -5881,6 +5929,8 @@ export class LlEmitter {
|
||||
// Process streams use a numeric descriptor internally, including zero
|
||||
// for stdin. They remain JavaScript objects in boolean contexts.
|
||||
case "procStream":
|
||||
// A module namespace object is always present.
|
||||
case "moduleNs":
|
||||
return "true";
|
||||
// A module namespace (a required builtin module object) is an object.
|
||||
case "moduleNs":
|
||||
|
||||
@@ -379,7 +379,29 @@ export function emitOperatorExpr(
|
||||
const fn = libm[e.op];
|
||||
if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
|
||||
host.declare(`declare double @${fn}(double, double)`);
|
||||
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
|
||||
// libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where
|
||||
// `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline
|
||||
// guard selects NaN for a base of ±1 with a non-finite exponent and
|
||||
// keeps the direct pow call. A finite literal exponent, or a literal
|
||||
// base other than ±1, cannot meet those cases and needs no guard.
|
||||
if (fn === "pow" && !libmPowAgrees(e.left, e.right)) {
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const power = B.tmp();
|
||||
B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`);
|
||||
const base = B.tmp();
|
||||
const unit = B.tmp();
|
||||
const magnitude = B.tmp();
|
||||
const finite = B.tmp();
|
||||
const reject = B.tmp();
|
||||
B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`);
|
||||
B.line(`${unit} = fcmp oeq double ${base}, 1.0`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`);
|
||||
B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`);
|
||||
B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`);
|
||||
B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`);
|
||||
} else {
|
||||
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
|
||||
}
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -602,7 +624,7 @@ export function emitStringExpr(
|
||||
}
|
||||
case "strCmp": {
|
||||
const [l, r] = emitStringInputs(host, [e.left, e.right]);
|
||||
const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp";
|
||||
const fn = "scr_str_cmp_u16";
|
||||
host.declare(`declare i32 @${fn}(ptr, ptr)`);
|
||||
const c = B.tmp();
|
||||
const t = B.tmp();
|
||||
@@ -1106,3 +1128,19 @@ export function emitRecordLitFields(
|
||||
host.initField(ptr, type, inline, v);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether libm pow matches `**` for these operands: every case except a
|
||||
* base of ±1 with a NaN or infinite exponent agrees. */
|
||||
function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean {
|
||||
const power = numericLiteral(exponent);
|
||||
if (power !== null && Number.isFinite(power)) return true;
|
||||
const value = numericLiteral(base);
|
||||
return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1;
|
||||
}
|
||||
|
||||
function numericLiteral(expr: IrExpr): number | null {
|
||||
if (expr.kind === "numLit") return expr.value;
|
||||
if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit")
|
||||
return -expr.operand.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -832,6 +832,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "util":
|
||||
case "bigint":
|
||||
case "crypto":
|
||||
case "webcrypto":
|
||||
case "buffer":
|
||||
case "bytes":
|
||||
case "tlsca":
|
||||
|
||||
@@ -546,15 +546,24 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
// A string listener takes the setEncoding-decoded chunk.
|
||||
const stringData = e.fn === "stdin.onData" && cbT.params[0]?.kind === "string";
|
||||
const adapter =
|
||||
e.fn === "stdin.onData"
|
||||
? cbT.params.length === 0
|
||||
? "scr_stdin_data_thunk0"
|
||||
: "scr_stdin_data_thunk_bytes"
|
||||
: stringData
|
||||
? "scr_stdin_data_thunk_str"
|
||||
: "scr_stdin_data_thunk_bytes"
|
||||
: cbT.params.length === 0
|
||||
? "scr_child_err_thunk0"
|
||||
: "scr_child_err_thunk_error";
|
||||
const sym = e.fn === "stdin.onData" ? "scr_stdin_on_data" : "scr_stdin_on_error";
|
||||
const sym =
|
||||
e.fn === "stdin.onData"
|
||||
? stringData
|
||||
? "scr_stdin_on_data_str"
|
||||
: "scr_stdin_on_data"
|
||||
: "scr_stdin_on_error";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @${sym}(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
|
||||
|
||||
@@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.parseInt": "scr_parse_int",
|
||||
"num.parseFloat": "scr_parse_float",
|
||||
"str.localeCompare": "scr_str_locale_compare",
|
||||
"str.substr": "scr_str_substr",
|
||||
"str.localeCase": "scr_str_locale_case",
|
||||
"str.replaceLiteral": "scr_str_replace_literal",
|
||||
"str.replaceChars": "scr_str_replace_chars",
|
||||
"num.fromString": "scr_string_to_number",
|
||||
@@ -143,6 +145,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toExponential": "scr_num_to_exponential",
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toPrecision": "scr_num_to_precision",
|
||||
"num.toExponentialDigits": "scr_num_to_exponential_digits",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
@@ -195,6 +199,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.cpusModel": "scr_os_cpus_model",
|
||||
"os.cpusField": "scr_os_cpus_field",
|
||||
"os.constants": "scr_os_constants",
|
||||
"fs.constants": "scr_fs_constants_object",
|
||||
"constants.object": "scr_node_constants",
|
||||
"os.release": "scr_os_release",
|
||||
"os.userName": "scr_os_user_name",
|
||||
@@ -408,6 +413,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.fromStr": "scr_bytes_from_str",
|
||||
"buffer.fromStrChecked": "scr_bytes_from_str_checked",
|
||||
"buffer.brand": "scr_bytes_as_buffer",
|
||||
"buffer.isBranded": "scr_bytes_is_buffer",
|
||||
"buffer.fromDyn": "scr_buffer_from_dyn",
|
||||
"buffer.concat": "scr_buffer_concat",
|
||||
"buffer.concatLen": "scr_buffer_concat_len",
|
||||
@@ -574,12 +580,19 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.randomIntMax": "scr_crypto_random_int_max",
|
||||
"crypto.randomFillView": "scr_crypto_random_fill",
|
||||
"crypto.randomFillViewRest": "scr_crypto_random_fill_rest",
|
||||
"crypto.getRandomValues": "scr_crypto_get_random_values",
|
||||
"crypto.getRandomValuesDyn": "scr_crypto_get_random_values_dyn",
|
||||
"crypto.randomFillSyncDyn": "scr_crypto_random_fill_sync_dyn",
|
||||
"crypto.randomUUIDOpts": "scr_crypto_random_uuid_opts",
|
||||
"crypto.createSecretKey": "scr_crypto_create_secret_key",
|
||||
"crypto.generateKeySync": "scr_crypto_generate_key_sync",
|
||||
"crypto.keyObjectInput": "scr_crypto_key_object_input",
|
||||
"crypto.isKeyObject": "scr_crypto_is_key_object",
|
||||
"webcrypto.importKey": "scr_webcrypto_import_key",
|
||||
"webcrypto.sign": "scr_webcrypto_sign",
|
||||
"webcrypto.verify": "scr_webcrypto_verify",
|
||||
"webcrypto.digest": "scr_webcrypto_digest",
|
||||
"webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key",
|
||||
"crypto.randomUUIDv7": "scr_crypto_random_uuid_v7",
|
||||
"crypto.pbkdf2": "scr_crypto_pbkdf2",
|
||||
"crypto.hkdf": "scr_crypto_hkdf",
|
||||
@@ -1328,6 +1341,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.emitWarningText": "scr_process_emit_warning_text",
|
||||
"process.deprecationFlag": "scr_process_deprecation_flag",
|
||||
"process.setDeprecationFlag": "scr_process_set_deprecation_flag",
|
||||
"process.setEmitWarning": "scr_process_set_emit_warning",
|
||||
"process.onUncaughtException": "scr_process_on_uncaught_exception",
|
||||
"process.offUncaughtException": "scr_process_off_uncaught_exception",
|
||||
"process.onUnhandledRejection": "scr_process_on_unhandled_rejection",
|
||||
@@ -1463,6 +1477,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"process.emitWarning",
|
||||
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
|
||||
"crypto.cipherNew",
|
||||
// WebCrypto results settle through the deferred crypto queue.
|
||||
"webcrypto.sign",
|
||||
"webcrypto.verify",
|
||||
"webcrypto.digest",
|
||||
"process.emitWarningText",
|
||||
// The legacy url entries queue DEP0169/DEP0170 on the next tick.
|
||||
"url.legacyParse",
|
||||
|
||||
@@ -432,7 +432,6 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners",
|
||||
op: "<",
|
||||
left: str("a"),
|
||||
right: str("b"),
|
||||
utf16: true,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
@@ -443,16 +442,14 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners",
|
||||
});
|
||||
|
||||
test("string comparisons decide identity, lengths and leading bytes before the runtime", () => {
|
||||
const order = (utf16: boolean) =>
|
||||
fn(`order${utf16 ? 16 : 8}`, [], {
|
||||
kind: "strCmp",
|
||||
op: "<",
|
||||
left: str("apple"),
|
||||
right: str("apricot"),
|
||||
utf16,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const order = fn("order", [], {
|
||||
kind: "strCmp",
|
||||
op: "<",
|
||||
left: str("apple"),
|
||||
right: str("apricot"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const same = fn("same", ["a", "b"], {
|
||||
kind: "strEq",
|
||||
left: { kind: "varRef", localId: "a", type: STRING, loc },
|
||||
@@ -462,23 +459,28 @@ test("string comparisons decide identity, lengths and leading bytes before the r
|
||||
loc,
|
||||
});
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = emitLlvmModule(mod(order(false), order(true), same), { pointerBits: bits });
|
||||
const ir = emitLlvmModule(mod(order, same), { pointerBits: bits });
|
||||
const helper = (name: string) =>
|
||||
new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir)![0];
|
||||
const eq = helper("sc_str_eq");
|
||||
expect(eq).toContain("icmp eq ptr %a, %b");
|
||||
expect(eq).toMatch(/icmp eq i(32|64) %alen, %blen/);
|
||||
expect(eq).toContain("call zeroext i1 @scr_str_eq(ptr %a, ptr %b)");
|
||||
const bytes = helper("sc_str_cmp");
|
||||
expect(bytes).toContain("@llvm.bswap.i64");
|
||||
expect(bytes).not.toContain("%ascii");
|
||||
expect(bytes).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
|
||||
const units = helper("sc_str_cmp_u16");
|
||||
expect(units).toContain("%wascii = icmp eq i64 %whigh, 0");
|
||||
expect(units).toContain("%tascii = icmp eq i64 %thigh, 0");
|
||||
expect(bytes).toContain("%tailable = and i1 %samelen, %short");
|
||||
expect(units).toContain("%ascii = icmp sge i8 %bits, 0");
|
||||
expect(units).toContain("call i32 @scr_str_cmp_u16(ptr %a, ptr %b)");
|
||||
expect(units).toContain("%dx = phi i64 [ %aw, %word ], [ %at, %tail ]");
|
||||
expect(units).toContain("%dlow = icmp eq i64 %whigh, 0");
|
||||
expect(units).toContain("@llvm.bswap.i64");
|
||||
expect(units).toContain("%dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)");
|
||||
expect(units).toContain("%bx = phi i8 [ %x, %first ], [ %dxb, %locate ]");
|
||||
expect(units).toContain("%blow = icmp ult i8 %bmin, -19");
|
||||
expect(units).toContain("%kbelow = icmp ult i8 %xk, %yk");
|
||||
expect(units).toContain("%below = icmp ult i8 %bx, %by");
|
||||
expect(units).toContain("%tailable = and i1 %samelen, %short");
|
||||
expect(units).toContain("%anyascii = icmp slt i32 %cws, 0");
|
||||
expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
|
||||
expect(units).toContain(
|
||||
"call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ export interface NativeLinkFeatures {
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
eventTarget?: boolean;
|
||||
/** The program assigns process.emitWarning: links the
|
||||
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
|
||||
warningReplace?: boolean;
|
||||
/** The program uses process.stdin.setEncoding, string 'data' listeners
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
zlib: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
@@ -51,6 +57,9 @@ export interface NativeLinkFeatures {
|
||||
* archive. */
|
||||
cipher: boolean;
|
||||
tlsCa: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
|
||||
* scr_webcrypto.c. */
|
||||
webcrypto: boolean;
|
||||
}
|
||||
|
||||
export interface NativeLinkInfo {
|
||||
|
||||
@@ -114,6 +114,10 @@ export interface CcOptions {
|
||||
* native fetch AbortSignal built on the same engine: compiles
|
||||
* scr_event_target.c. */
|
||||
eventTarget?: boolean;
|
||||
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
|
||||
warningReplace?: boolean;
|
||||
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
|
||||
stdioExtended?: boolean;
|
||||
/** The program uses the process-events surface (signal/exit listeners,
|
||||
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
|
||||
* IR): compiles scr_events.c into the binary. Event-free binaries keep
|
||||
@@ -199,6 +203,8 @@ export interface CcOptions {
|
||||
/** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links
|
||||
* the mbedTLS archive (shared with tls). */
|
||||
cipher?: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
|
||||
webcrypto?: boolean;
|
||||
/** Internal compiler hook: called only after a strict native artifact hit
|
||||
* or a successful stable build has installed `outPath`. The executable
|
||||
* frontend cache uses it to publish its stamp after native dependencies
|
||||
|
||||
@@ -445,6 +445,8 @@ export async function compileCInternal(
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
"-fno-math-errno",
|
||||
// The runtime object model uses type-punned C: a hierarchy
|
||||
@@ -578,6 +580,7 @@ export async function compileCInternal(
|
||||
...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
|
||||
...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
|
||||
...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
|
||||
...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
// references its default-set override unconditionally.
|
||||
...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
|
||||
@@ -644,6 +647,8 @@ export async function compileCInternal(
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
"-fno-math-errno",
|
||||
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
|
||||
|
||||
@@ -82,6 +82,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
workers: boolean;
|
||||
youngNursery: boolean;
|
||||
eventTarget: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
nativeFetch: boolean;
|
||||
netEffective: boolean;
|
||||
httpEffective: boolean;
|
||||
@@ -269,6 +271,8 @@ export function effectiveRuntimeFeatures(
|
||||
workers: features.workers === true,
|
||||
youngNursery: features.youngNursery === true,
|
||||
eventTarget: features.eventTarget === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
nativeFetch,
|
||||
netEffective,
|
||||
httpEffective,
|
||||
|
||||
@@ -119,6 +119,7 @@ const features = {
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
cipher: false,
|
||||
webcrypto: false,
|
||||
};
|
||||
|
||||
test.each([
|
||||
|
||||
@@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = {
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
cipher: false,
|
||||
webcrypto: false,
|
||||
};
|
||||
|
||||
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function warmNativeCaches(
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
cipher: false,
|
||||
webcrypto: false,
|
||||
tlsCa: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
test("unchanged and comment-only reruns replay the verdict; code edits relower", () => {
|
||||
const entry = join(dir, "main.ts");
|
||||
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`;
|
||||
// The blocker: an object pattern over a union source (arrays alone compile).
|
||||
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`;
|
||||
writeFileSync(entry, program);
|
||||
const entries = new Map<string, string>();
|
||||
const store: VerdictStore = {
|
||||
|
||||
@@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
cipher: false,
|
||||
webcrypto: false,
|
||||
};
|
||||
|
||||
async function fixture(): Promise<{
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
eventTarget?: boolean;
|
||||
warningReplace?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
zlib: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
@@ -80,6 +82,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
tls: boolean;
|
||||
cipher: boolean;
|
||||
tlsCa: boolean;
|
||||
webcrypto: boolean;
|
||||
}
|
||||
|
||||
interface EarlyExecutableCacheStamp {
|
||||
@@ -193,6 +196,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"tls",
|
||||
"tlsCa",
|
||||
"cipher",
|
||||
"webcrypto",
|
||||
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
|
||||
|
||||
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
|
||||
|
||||
@@ -109,7 +109,7 @@ function markerNamed(
|
||||
if (!MARKERS.has(written)) return null;
|
||||
let member = written;
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined) return null;
|
||||
let importDecl: ts.Node | undefined;
|
||||
if (ts.isImportSpecifier(decl) && !qualified) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { npmPackageNameOf } from "./workspace-registry.js";
|
||||
import { tsgoPath } from "./ts7/session-path.js";
|
||||
|
||||
@@ -25,6 +25,15 @@ function declarationPath(name: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** Directory of the shipped package shims (packages/compiler/shims): the
|
||||
* implementation modules package-shims.ts maps npm packages to. A native
|
||||
* distribution ships them beside its declarations. */
|
||||
export function packageShimRoot(): string {
|
||||
return installedDeclarationRoot === null
|
||||
? join(dirname(require.resolve("@scriptc/compiler/scriptc.d.ts")), "..", "shims")
|
||||
: join(installedDeclarationRoot, "shims");
|
||||
}
|
||||
|
||||
/** Path of the shipped ambient declarations — the always-shipped CORE
|
||||
* (comptime, setTimeout). Part of EVERY program scriptc
|
||||
* builds, the project-world preflight program included. */
|
||||
|
||||
@@ -18,7 +18,7 @@ export function experimentalImportOf(
|
||||
): ExperimentalMember | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined || !ts.isImportSpecifier(decl)) return null;
|
||||
const importDecl = decl.parent?.parent?.parent;
|
||||
if (
|
||||
@@ -67,7 +67,7 @@ export function lifetimeRegionMemberOf(
|
||||
): "run" | "release" | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(access.name);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
|
||||
const owner = decl.parent;
|
||||
if (
|
||||
@@ -91,7 +91,7 @@ export function internTableMemberOf(
|
||||
): InternTableMember | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(access.name);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!(
|
||||
@@ -129,7 +129,7 @@ export function threadSlotsMemberOf(
|
||||
): ThreadSlotsMember | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(access.name);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
|
||||
const owner = decl.parent;
|
||||
if (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(expression);
|
||||
const declarations = symbol ? checker.declarationsOf(symbol) : [];
|
||||
const declarations = symbol ? symbol.declarations : [];
|
||||
return (
|
||||
declarations.length > 0 &&
|
||||
declarations.every((declaration) => {
|
||||
@@ -45,7 +45,7 @@ export function isBuiltinMemberImport(
|
||||
): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl !== undefined && ts.isImportSpecifier(decl)) {
|
||||
const importDecl = decl.parent?.parent?.parent;
|
||||
if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
||||
@@ -60,7 +60,7 @@ export function isBuiltinMemberImport(
|
||||
}
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||
const target = checker.getAliasedSymbol(symbol);
|
||||
const targetDecl = checker.declarationsOf(target)[0];
|
||||
const targetDecl = target.declarations[0];
|
||||
if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
|
||||
for (
|
||||
let parent: ts.Node | undefined = targetDecl.parent;
|
||||
@@ -88,7 +88,7 @@ export function isBuiltinNamespaceImport(
|
||||
): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
let importDecl: ts.ImportDeclaration | undefined;
|
||||
if (decl !== undefined && ts.isNamespaceImport(decl)) {
|
||||
const candidate = decl.parent?.parent;
|
||||
@@ -108,12 +108,9 @@ function constInitializer(program: ts.Program, expr: ts.Expression): ts.Expressi
|
||||
if (!ts.isIdentifier(current)) return null;
|
||||
const symbol = program.getTypeChecker().getSymbolAtLocation(current);
|
||||
const decl = symbol
|
||||
? program
|
||||
.getTypeChecker()
|
||||
.declarationsOf(symbol)
|
||||
.find((candidate): candidate is ts.VariableDeclaration =>
|
||||
ts.isVariableDeclaration(candidate),
|
||||
)
|
||||
? symbol.declarations.find((candidate): candidate is ts.VariableDeclaration =>
|
||||
ts.isVariableDeclaration(candidate),
|
||||
)
|
||||
: undefined;
|
||||
if (
|
||||
decl === undefined ||
|
||||
|
||||
@@ -25,15 +25,13 @@ export function inferredJsDiagnosticSuppressed(
|
||||
const symbol = checker.getSymbolAtLocation(node.name);
|
||||
if (
|
||||
symbol &&
|
||||
checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(decl) =>
|
||||
ts.isPropertyDeclaration(decl) &&
|
||||
!decl.type &&
|
||||
!decl.initializer &&
|
||||
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
|
||||
)
|
||||
symbol.declarations.some(
|
||||
(decl) =>
|
||||
ts.isPropertyDeclaration(decl) &&
|
||||
!decl.type &&
|
||||
!decl.initializer &&
|
||||
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
|
||||
)
|
||||
) {
|
||||
// Bare JS fields exist with value undefined before the first write;
|
||||
// native storage includes that arm even when inference omits it.
|
||||
@@ -43,7 +41,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
}
|
||||
if (diagnostic.code === 2554 && ts.isCallExpression(node)) {
|
||||
const signature = checker.getResolvedSignature(node);
|
||||
const declaration = signature && checker.signatureDeclaration(signature);
|
||||
const declaration = signature && signature.declaration;
|
||||
if (
|
||||
declaration &&
|
||||
isJsSourceFileName(declaration.getSourceFile().fileName) &&
|
||||
@@ -83,9 +81,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
checker.getTypeOfSymbolAtLocation(throwMethod, node.expression),
|
||||
);
|
||||
const declaration =
|
||||
throwSignatures.length === 1
|
||||
? checker.signatureDeclaration(throwSignatures[0]!)
|
||||
: undefined;
|
||||
throwSignatures.length === 1 ? throwSignatures[0]!.declaration : undefined;
|
||||
// A sole throw cannot return an invalid IteratorResult. Avoid inferring
|
||||
// termination through arbitrary control flow or explicit return types.
|
||||
if (
|
||||
|
||||
@@ -176,7 +176,7 @@ export function assertInstanceOf(
|
||||
): AssertInstanceOptions | null {
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return null;
|
||||
return staticAssertInstanceDecl(lowerer, decl.name, decl.initializer);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Type assertions of the global object.
|
||||
*
|
||||
* `globalThis as { [KEY]?: State }` is the process-global state idiom: a
|
||||
* module asserts the global object to a record type and reads and writes
|
||||
* its members, and every other module that asserts it sees the same
|
||||
* members. Node evaluates the assertion to the global object itself.
|
||||
*
|
||||
* The global object is a checked-dynamic value, and a checked-dynamic value
|
||||
* asserted to a record or array type takes the checked extraction
|
||||
* (dynCheck), which builds a new native value. For the global object that
|
||||
* copy is always wrong: a write through it never reached the global object,
|
||||
* and a view bound before another module's write never saw it. So an
|
||||
* assertion of the global object keeps the checked-dynamic value itself: a
|
||||
* view whose member reads and writes go to the global object, and bindings
|
||||
* of it hold that object.
|
||||
*
|
||||
* Other checked-dynamic operands keep the checked extraction: a native value
|
||||
* held as `unknown` unwraps by identity there, and a checked-dynamic tree is
|
||||
* copied at the documented checked-value boundary.
|
||||
*/
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { stdlibGlobalNameOf } from "./surfaces.js";
|
||||
|
||||
/** A target whose checked extraction builds a new mutable value. */
|
||||
export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean {
|
||||
const seen = new Set<string>();
|
||||
const copies = (t: IrType): boolean => {
|
||||
if (t.kind === "record" || t.kind === "array") return true;
|
||||
if (t.kind !== "union" || seen.has(t.unionId)) return false;
|
||||
seen.add(t.unionId);
|
||||
return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false;
|
||||
};
|
||||
return copies(type);
|
||||
}
|
||||
|
||||
/** The asserted expression without parentheses, `!` and nested
|
||||
* assertions (`globalThis as unknown as T`). */
|
||||
function assertedSource(expr: ts.Expression): ts.Expression {
|
||||
let e = expr;
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isNonNullExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isSatisfiesExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** `expr` asserts the global object (`globalThis`, `global`). */
|
||||
export function assertsGlobalObject(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AsExpression | ts.TypeAssertion,
|
||||
): boolean {
|
||||
const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression));
|
||||
return name === "globalThis" || name === "global";
|
||||
}
|
||||
|
||||
/** The assertion a binding initializer is, when it keeps the global object:
|
||||
* bindings of it hold that object rather than a native value of the
|
||||
* asserted type. A target with no native layout keeps the object as well
|
||||
* (the assertion lowers to the checked-dynamic value either way). */
|
||||
export function keptIdentityAssertion(
|
||||
lowerer: Lowerer,
|
||||
initializer: ts.Expression,
|
||||
): ts.AsExpression | ts.TypeAssertion | undefined {
|
||||
let e = initializer;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined;
|
||||
if (!assertsGlobalObject(lowerer, e)) return undefined;
|
||||
const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type));
|
||||
return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target)
|
||||
? e
|
||||
: undefined;
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export function nullishValueUnitOf(
|
||||
const cached = lowerer.nullishBindings.get(sym);
|
||||
if (cached !== undefined) return cached;
|
||||
lowerer.nullishBindings.set(sym, null); // cycle guard: self-referential probes answer non-qualifying
|
||||
const decl = lowerer.checker.valueDeclarationOf(sym);
|
||||
const decl = sym.valueDeclaration;
|
||||
// Statement-position declarators only: a for-loop head (`for (let x =
|
||||
// null as any; ...)`) declares a LOCAL with per-iteration semantics —
|
||||
// lowerVarDeclList's contract requires a lowered statement for it, so
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/** A builtin module function used as a JavaScript value when it has no
|
||||
* value adapter (lowerNativeCallableValue). Node hands the program a real
|
||||
* function; the compiled value is one function per builtin that throws the
|
||||
* named fence when called. Identity flows (`fn === fs.readFileSync`, Set
|
||||
* membership) still hold, `typeof` answers "function" and inspection
|
||||
* prints `[Function: name]`, where a string token would answer "string"
|
||||
* and turn a call into an ordinary "is not a function" TypeError. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, type IrExpr, type SrcLoc, funcOf } from "../../ir/ir.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
export function builtinFunctionFenceValue(
|
||||
lowerer: Lowerer,
|
||||
bi: { module: string; member: string },
|
||||
node: ts.Identifier,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const display = `${bi.module}.${bi.member}`;
|
||||
const name = `%builtin.fence.${display}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const diag = unsupportedDiag(
|
||||
"SC1090",
|
||||
loc,
|
||||
`library functions as values ('${display}' has no function-value lowering; call it directly)`,
|
||||
);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1;
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: diag.code,
|
||||
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
|
||||
fnName: bi.member,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression
|
||||
// has no other first-class story.
|
||||
if (ts.isIdentifier(node) && node.text === "setImmediate") {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
|
||||
const decls = sym ? sym.declarations : [];
|
||||
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
|
||||
@@ -93,7 +93,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol)
|
||||
if (builtinClassAliasOf(lowerer, symbol) === null) return false;
|
||||
const binding =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const decl = lowerer.checker.valueDeclarationOf(binding);
|
||||
const decl = binding.valueDeclaration;
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
|
||||
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
|
||||
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
|
||||
@@ -189,7 +189,7 @@ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolea
|
||||
* can name a class (an identifier or a property access), or null. */
|
||||
function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
const decl = symbol.valueDeclaration;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
|
||||
@@ -138,9 +138,7 @@ export function zlibStreamModeOf(
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const mode = ZLIB_STREAM_MODES.get(target.name);
|
||||
if (mode === undefined) return null;
|
||||
return lowerer.checker
|
||||
.declarationsOf(target)
|
||||
.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
|
||||
return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
|
||||
? mode
|
||||
: null;
|
||||
}
|
||||
@@ -161,15 +159,13 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
|
||||
export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
if (!symbol) return false;
|
||||
return lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
d.parent !== undefined &&
|
||||
ts.isClassDeclaration(d.parent) &&
|
||||
d.parent.name?.text === "ZlibBase" &&
|
||||
isZlibFallbackDecl(lowerer, d.parent),
|
||||
);
|
||||
return symbol.declarations.some(
|
||||
(d) =>
|
||||
d.parent !== undefined &&
|
||||
ts.isClassDeclaration(d.parent) &&
|
||||
d.parent.name?.text === "ZlibBase" &&
|
||||
isZlibFallbackDecl(lowerer, d.parent),
|
||||
);
|
||||
}
|
||||
|
||||
/** `stream.bytesWritten` on a zlib stream. */
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js";
|
||||
import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../../program.js";
|
||||
import { builtinConstLit } from "../surfaces.js";
|
||||
import { HTTP2_CONSTANTS } from "../http2-constants.js";
|
||||
import { CRYPTO_CONSTANTS } from "../crypto-tables.js";
|
||||
import { ZLIB_CONSTANTS } from "../zlib-constants.js";
|
||||
import { F64, type IrExpr } from "../../../ir/ir.js";
|
||||
import { DYN, F64, type IrExpr, STRING, type SrcLoc } from "../../../ir/ir.js";
|
||||
import { builtinImportOf } from "./module-bindings.js";
|
||||
|
||||
/** The builtin modules whose `constants` object bakes as literals at
|
||||
@@ -106,7 +106,7 @@ export function builtinConstantsDestructureDecl(
|
||||
* its baked literal value. Null for every other binding. */
|
||||
export function builtinConstantBindingOf(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (!decl || !ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null;
|
||||
if (!ts.isObjectBindingPattern(decl.parent)) return null;
|
||||
const varDecl = decl.parent.parent;
|
||||
@@ -147,12 +147,30 @@ export function lowerFsConstantsProperty(
|
||||
COPYFILE_FICLONE_FORCE: 4,
|
||||
};
|
||||
const value = own(MODES, expr.name.text);
|
||||
if (value === undefined) {
|
||||
lowerer.noLowering(
|
||||
`fs.constants.${expr.name.text}`,
|
||||
expr,
|
||||
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
|
||||
);
|
||||
}
|
||||
if (value === undefined) return fsConstantMemberRead(expr);
|
||||
return { kind: "numLit", value, type: F64, loc: locOf(expr) };
|
||||
}
|
||||
|
||||
/** fs.constants as a value: the target's null-prototype constants object,
|
||||
* built by the runtime from its own libc's macros. */
|
||||
export function fsConstantsObject(loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** `fs.constants.O_RDONLY` (any member the access-mode literals do not
|
||||
* cover): a read of the runtime object, so a constant the target lacks
|
||||
* reads as undefined in JavaScript and fails the number check in
|
||||
* TypeScript, whose declarations promise every name. */
|
||||
export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const read: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: fsConstantsObject(loc),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return isJsSourceFile(expr.getSourceFile())
|
||||
? read
|
||||
: { kind: "dynCheck", value: read, type: F64, loc };
|
||||
}
|
||||
|
||||
@@ -263,6 +263,7 @@ export function lowerCryptoModuleCall(
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr);
|
||||
}
|
||||
if (bi.member === "getRandomValues") return lowerCryptoGetRandomValues(lowerer, expr);
|
||||
if (bi.member === "randomBytes") {
|
||||
if (args.length === 1) return null;
|
||||
if (args.length !== 2) {
|
||||
@@ -878,7 +879,7 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
return (
|
||||
!!symbol &&
|
||||
CIPHER_TYPE_NAMES.has(symbol.name) &&
|
||||
lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile()))
|
||||
symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1284,3 +1285,49 @@ export function lowerPromisifiedCryptoCall(
|
||||
"KeyObject",
|
||||
);
|
||||
}
|
||||
|
||||
/** `crypto.getRandomValues(view)` — the WebCrypto global, node:crypto's
|
||||
* export and `webcrypto.getRandomValues`: a typed view fills in place and
|
||||
* returns itself at its own type (float views, DataViews and views over
|
||||
* 65536 bytes throw Node's DOMExceptions at run time); any other value
|
||||
* takes the checked form. Null for other arities (the member fence keeps
|
||||
* them). */
|
||||
export function lowerCryptoGetRandomValues(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!))
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
const value = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
if (value.type.kind === "bytes")
|
||||
return { kind: "libCall", fn: "crypto.getRandomValues", args: [value], type: value.type, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "crypto.getRandomValuesDyn",
|
||||
args: [lowerer.coerceInto(call.arguments.nodes[0]!, value, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether `receiver` names a WebCrypto Crypto object: the `crypto`
|
||||
* global (also through globalThis) or node:crypto's `webcrypto`. */
|
||||
export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean {
|
||||
if (ts.isIdentifier(receiver)) {
|
||||
if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true;
|
||||
const imported = lowerer.builtinImportOf(receiver);
|
||||
return imported?.module === "crypto" && imported.member === "webcrypto";
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(receiver) && !receiver.questionDotToken) {
|
||||
if (
|
||||
receiver.name.text === "crypto" &&
|
||||
ts.isIdentifier(receiver.expression) &&
|
||||
lowerer.isStdlibGlobal(receiver.expression, "globalThis")
|
||||
)
|
||||
return true;
|
||||
const member = lowerer.builtinMemberOf(receiver);
|
||||
return member?.module === "crypto" && member.member === "webcrypto";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
|
||||
const sym = t.getSymbol();
|
||||
return (
|
||||
sym?.name === "ExecException" &&
|
||||
lowerer.checker
|
||||
.declarationsOf(sym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
|
||||
sym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,15 +34,13 @@ function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
|
||||
export function isFsStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
if (!symbol) return false;
|
||||
return lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
d.parent !== undefined &&
|
||||
ts.isClassDeclaration(d.parent) &&
|
||||
(d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") &&
|
||||
isFsFallbackDecl(lowerer, d.parent),
|
||||
);
|
||||
return symbol.declarations.some(
|
||||
(d) =>
|
||||
d.parent !== undefined &&
|
||||
ts.isClassDeclaration(d.parent) &&
|
||||
(d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") &&
|
||||
isFsFallbackDecl(lowerer, d.parent),
|
||||
);
|
||||
}
|
||||
|
||||
/** bytesRead / bytesWritten / pending / path reads. Null for other
|
||||
@@ -156,9 +154,9 @@ export function fsStreamClassOf(
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): "ReadStream" | "WriteStream" | null {
|
||||
if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null;
|
||||
const declared = lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some((d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d));
|
||||
const declared = symbol.declarations.some(
|
||||
(d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d),
|
||||
);
|
||||
return declared ? symbol.name : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -504,9 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
|
||||
symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export function builtinImportOf(
|
||||
ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
|
||||
? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent)
|
||||
: lowerer.checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (!decl) return null;
|
||||
// The CommonJS twin of the named import: a destructured require
|
||||
// binding (`const { readFileSync } = require("fs")`, renames via
|
||||
@@ -123,7 +123,7 @@ export function builtinImportOf(
|
||||
// keys the same tables under the builtin's own member name.
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||
const target = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const tdecl = lowerer.checker.declarationsOf(target)[0];
|
||||
const tdecl = target.declarations[0];
|
||||
if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) {
|
||||
for (
|
||||
let p: ts.Node | undefined = tdecl.parent;
|
||||
@@ -153,7 +153,7 @@ export function esmImportBindingKind(
|
||||
ident: ts.Identifier,
|
||||
): "namespace" | "named" | null {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined) return null;
|
||||
if (ts.isNamespaceImport(decl)) return "namespace";
|
||||
return ts.isImportSpecifier(decl) ? "named" : null;
|
||||
@@ -290,7 +290,7 @@ export function createRequireCalleeFileOf(
|
||||
}
|
||||
if (!ts.isIdentifier(e)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(e);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null;
|
||||
return createRequireBindingDecl(lowerer, decl.name, decl.initializer)
|
||||
? decl.getSourceFile()
|
||||
@@ -391,7 +391,7 @@ export function createRequireProgramModuleDecl(
|
||||
* binding poison with the same module-naming diagnostic. */
|
||||
export function fencedBuiltinImportOf(lowerer: Lowerer, ident: ts.Identifier): string | null {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (!decl) return null;
|
||||
// The CommonJS twins: `const x = require("net")` and
|
||||
// `const { createServer } = require("net")` — the require statement
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* Node 24's `navigator` global (lib/internal/navigator.js), the part
|
||||
* libraries read to tell runtimes apart: `typeof navigator` is "object"
|
||||
* and `navigator.userAgent` is `Node.js/<major>`. jose reads exactly
|
||||
* that at module load (jwks/remote.js). Both fold to string literals, so
|
||||
* nothing links. The other members (hardwareConcurrency, language,
|
||||
* languages, platform, locks) and the object as a value keep a named
|
||||
* refusal. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { strLit } from "../../../ir/build.js";
|
||||
import type { IrExpr } from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
|
||||
/** True when `node` names Node's global `navigator`: the @types/node
|
||||
* declaration, or, in a JavaScript file, the free name no declaration
|
||||
* binds (Node defines it on the global object). */
|
||||
export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let expr = node;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
|
||||
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
|
||||
return (
|
||||
isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
|
||||
);
|
||||
}
|
||||
|
||||
/** `navigator.userAgent` (`Node.js/<major>` of `nodeVersion`, the pinned
|
||||
* release builtins/process.ts names), or the named refusal for another
|
||||
* member. Null when the receiver is not the global. */
|
||||
export function lowerNavigatorMember(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
nodeVersion: string,
|
||||
): IrExpr | null {
|
||||
if (!isNavigatorGlobal(lowerer, access.expression)) return null;
|
||||
if (access.name.text === "userAgent") {
|
||||
return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access));
|
||||
}
|
||||
return refuseNavigator(
|
||||
lowerer,
|
||||
access,
|
||||
`navigator.${access.name.text}`,
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
/** The refusal for the `navigator` object itself or an unlowered member. */
|
||||
export function refuseNavigator(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
what: string,
|
||||
symbol?: ts.Symbol,
|
||||
): never {
|
||||
return lowerer.noLowering(
|
||||
what,
|
||||
node,
|
||||
"of Node's navigator global only typeof navigator and navigator.userAgent compile",
|
||||
symbol ?? null,
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
|
||||
import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
|
||||
@@ -148,6 +149,18 @@ export function lowerProcessStreamProperty(
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// `if (process.stdin.setRawMode)`: Node's stdin has the method only
|
||||
// when it is a TTY. The runtime's stream object answers the same
|
||||
// presence (the method or undefined); calls keep their direct lowering.
|
||||
if (direct === "stdin" && member === "setRawMode") {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.lowerExpr(expr.expression),
|
||||
key: strLit(member, locOf(expr.name)),
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
if (member !== "isTTY" && member !== "columns" && member !== "rows") return null;
|
||||
let recv: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv))
|
||||
@@ -364,6 +377,11 @@ export function lowerProcessProperty(
|
||||
if (member === "versions") {
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
}
|
||||
// process.features as a value (`"sea" in process.features`): an object
|
||||
// holding none of the flags, consistent with the member reads above.
|
||||
if (member === "features") {
|
||||
return { kind: "dynObjLit", type: DYN, loc };
|
||||
}
|
||||
// process.noDeprecation / process.throwDeprecation: the values the
|
||||
// program assigned (undefined before that — no Node flags reach a
|
||||
// compiled binary).
|
||||
@@ -627,6 +645,73 @@ export function lowerProcessMethodCall(
|
||||
}
|
||||
return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc };
|
||||
}
|
||||
// process.stdout.on("resize", cb) (and once/off/removeListener/
|
||||
// addListener, stderr too): the terminal resize event, registered on the
|
||||
// runtime's process stream object (a SIGWINCH listener that emits when a
|
||||
// TTY's size changed; it never keeps the loop alive). Statement position:
|
||||
// Node returns the stream.
|
||||
if (
|
||||
["on", "once", "off", "removeListener", "addListener"].includes(access.name.text) &&
|
||||
ts.isPropertyAccessExpression(access.expression) &&
|
||||
call.arguments.nodes.length === 2 &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement) &&
|
||||
ts.isExpressionStatement(call.parent)
|
||||
) {
|
||||
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
|
||||
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
|
||||
if (
|
||||
(stream === "stdout" || stream === "stderr") &&
|
||||
evT.isStringLiteralType() &&
|
||||
evT.value === "resize"
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%stdioReceiver", DYN);
|
||||
const receiver = varRef(receiverLocal.id, DYN, loc);
|
||||
const method =
|
||||
access.name.text === "off"
|
||||
? "removeListener"
|
||||
: access.name.text === "addListener"
|
||||
? "on"
|
||||
: access.name.text;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: receiverLocal.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "process.stdio",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "dynCall",
|
||||
callee: {
|
||||
kind: "dynKeyGet",
|
||||
value: receiver,
|
||||
key: strLit(method, locOf(access.name)),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
receiver,
|
||||
calleeName: access.getText(),
|
||||
args: [
|
||||
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN),
|
||||
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// process.stdin.on/once("data" | "end" | "error", cb): the piped-stdin
|
||||
// event slice. A 'data' listener keeps the event loop alive until EOF
|
||||
// (Node's flowing stdin); 'end'/'error' listeners alone do not. `once`
|
||||
@@ -678,11 +763,18 @@ export function lowerProcessMethodCall(
|
||||
const param = cb.type.params[0];
|
||||
const onceArg: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc };
|
||||
if (event === "data") {
|
||||
if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) {
|
||||
// A string parameter receives the chunks process.stdin.setEncoding
|
||||
// decodes (pi-tui's terminal input); the runtime refuses a delivery
|
||||
// while no encoding is set, where Node would pass a Buffer.
|
||||
if (
|
||||
param !== undefined &&
|
||||
!(param.kind === "bytes" && param.elem === "u8") &&
|
||||
param.kind !== "string"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call.arguments.nodes[1]!,
|
||||
`data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`,
|
||||
`data listeners whose parameter is not 'Uint8Array' or 'string' (got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "stdin.onData", args: [cb, onceArg], type: VOID, loc };
|
||||
@@ -1537,3 +1629,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `process.getBuiltinModule("node:os")`: a string-literal id naming a
|
||||
* supported builtin whose call the checker types as that module's
|
||||
* namespace (@types/node's BuiltInModule map) answers the namespace value
|
||||
* `import * as ns` binds, so its members key the builtin tables at their
|
||||
* use sites. The receiver may be cast (`(process as P)`) and the call
|
||||
* optional: Node 24's process always has the member, and the cast changes
|
||||
* no runtime value. Other ids keep the dynamic loader (undefined for
|
||||
* unknown ids, Node's argument errors). */
|
||||
export function lowerProcessBuiltinModuleNamespace(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule")
|
||||
return null;
|
||||
const id = call.arguments.nodes[0];
|
||||
if (call.arguments.nodes.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null;
|
||||
let receiver: ts.Expression = callee.expression;
|
||||
while (
|
||||
ts.isParenthesizedExpression(receiver) ||
|
||||
ts.isAsExpression(receiver) ||
|
||||
ts.isSatisfiesExpression(receiver) ||
|
||||
ts.isNonNullExpression(receiver)
|
||||
)
|
||||
receiver = receiver.expression;
|
||||
if (!lowerer.isStdlibGlobal(receiver, "process")) return null;
|
||||
const module = canonicalBuiltinModule(id.text);
|
||||
if (module === null) return null;
|
||||
const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call)));
|
||||
if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null;
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) };
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== "Interface") return false;
|
||||
return lowerer.checker.declarationsOf(sym).some((d) => {
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
return isReadlineModule(ambientModuleOf(d));
|
||||
@@ -107,7 +107,7 @@ function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== "Readline") return false;
|
||||
return lowerer.checker.declarationsOf(sym).some((d) => {
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
const spec = ambientModuleOf(d);
|
||||
|
||||
@@ -11,11 +11,9 @@ export function isBuiltinClassInstance(
|
||||
const type = lowerer.checker.getTypeAtLocation(node);
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (symbol?.name !== name) return false;
|
||||
return lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(declaration) =>
|
||||
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
|
||||
lowerer.isStdlibFile(declaration.getSourceFile()),
|
||||
);
|
||||
return symbol.declarations.some(
|
||||
(declaration) =>
|
||||
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
|
||||
lowerer.isStdlibFile(declaration.getSourceFile()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
NULL_T,
|
||||
STRING,
|
||||
arrayOf,
|
||||
bytesOf,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
/** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument
|
||||
* lowers as a number and packs into ONE f64[] array-literal argument
|
||||
@@ -17,6 +29,8 @@ export function lowerStringStaticCall(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken) return null;
|
||||
const applied = lowerFromCharCodeApply(lowerer, call, access);
|
||||
if (applied) return applied;
|
||||
const member = lowerer.stdlibGlobalMember(access, "String");
|
||||
if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null;
|
||||
const loc = locOf(call);
|
||||
@@ -108,6 +122,99 @@ export function lowerStringStaticCall(
|
||||
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom
|
||||
* (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver
|
||||
* is ignored, and a typed array or number array forwards to the packed
|
||||
* fromCharCode like a spread. An untyped list reads a Uint8Array the same
|
||||
* way at run time; any other value keeps Function.prototype.apply's
|
||||
* generic array-like call. Null for every other call. */
|
||||
function lowerFromCharCodeApply(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "apply" || access.questionDotToken) return null;
|
||||
const target = access.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(target) ||
|
||||
lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement))
|
||||
return null;
|
||||
const listNode = call.arguments.nodes[1]!;
|
||||
const checkerT = lowerer.typeOf(listNode);
|
||||
const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
||||
const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT);
|
||||
if (
|
||||
listT === null ||
|
||||
(listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64)))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN);
|
||||
const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }];
|
||||
if (listT.kind !== "dyn") {
|
||||
const list = lowerer.lowerExprExpecting(listNode, listT);
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "string.fromCharCode",
|
||||
args: [list],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return { kind: "seqExpr", stmts, result, type: STRING, loc };
|
||||
}
|
||||
const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: listLocal.id,
|
||||
init: lowerer.lowerExprExpecting(listNode, DYN),
|
||||
loc,
|
||||
});
|
||||
const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc });
|
||||
const bytes = bytesOf("u8");
|
||||
const generic: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(target, DYN),
|
||||
method: "apply",
|
||||
calleeName: "String.fromCharCode.apply",
|
||||
args: [
|
||||
{
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit: "null", type: NULL_T, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
list(),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const result: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc },
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "string.fromCharCode",
|
||||
args: [{ kind: "dynCheck", value: list(), type: bytes, loc }],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
else_: generic,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return { kind: "seqExpr", stmts, result, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16
|
||||
* indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end;
|
||||
* MAX_SAFE_INTEGER has the same effect for every representable string.
|
||||
|
||||
@@ -361,13 +361,11 @@ export function storedTextCodecClassOf(
|
||||
): TextCodecCtor["cls"] | null {
|
||||
const sym = lowerer.typeOf(expr).getSymbol();
|
||||
if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null;
|
||||
return lowerer.checker
|
||||
.declarationsOf(sym)
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
return sym.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
? sym.name
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -155,9 +155,13 @@ function isTimerHandleTyped(
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== name) return false;
|
||||
return lowerer.checker
|
||||
.declarationsOf(sym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()));
|
||||
return sym.declarations.some(
|
||||
(d) =>
|
||||
// An interface in the fallback and @types/node 22+, a class in
|
||||
// @types/node 20 and older.
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
);
|
||||
}
|
||||
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return isTimerHandleTyped(lowerer, node, "Timeout");
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js";
|
||||
|
||||
/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`,
|
||||
* `sign`, `verify` and `digest` called on the crypto global (also through
|
||||
* globalThis) lower to runtime entry points that take the argument vector
|
||||
* as one dyn array and answer a promise, so Web IDL conversion, algorithm
|
||||
* normalization and every error stay Node-shaped at run time. CryptoKey
|
||||
* values are native handles; their type maps to dyn and their members
|
||||
* are handle reads. Other SubtleCrypto methods are refused by name. */
|
||||
|
||||
const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
["importKey", "webcrypto.importKey"],
|
||||
["sign", "webcrypto.sign"],
|
||||
["verify", "webcrypto.verify"],
|
||||
["digest", "webcrypto.digest"],
|
||||
]);
|
||||
|
||||
const SUBTLE_HINT =
|
||||
"the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
|
||||
|
||||
/** Whether `node` is `crypto.subtle` on the WebCrypto global. */
|
||||
function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
!node.questionDotToken &&
|
||||
node.name.text === "subtle" &&
|
||||
stdlibGlobalNameOf(lowerer, node.expression) === "crypto"
|
||||
);
|
||||
}
|
||||
|
||||
/** `crypto.subtle.<method>(...args)`: the lowered methods become their
|
||||
* libCall over the argument vector; every other method is refused. */
|
||||
export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) {
|
||||
return null;
|
||||
}
|
||||
const member = callee.name.text;
|
||||
const fn = SUBTLE_LOWERED.get(member);
|
||||
if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT);
|
||||
if (callee.questionDotToken || call.questionDotToken) {
|
||||
lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT);
|
||||
}
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT);
|
||||
}
|
||||
// JsonWebKey imports are not implemented: a literal 'jwk' format is
|
||||
// refused here (the runtime refuses a computed one).
|
||||
const format = call.arguments.nodes[0];
|
||||
if (
|
||||
member === "importKey" &&
|
||||
format !== undefined &&
|
||||
ts.isStringLiteralLike(format) &&
|
||||
format.text === "jwk"
|
||||
) {
|
||||
lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT);
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes.map((arg) =>
|
||||
ts.isObjectLiteralExpression(arg)
|
||||
? lowerDynObjectLiteral(lowerer, arg)
|
||||
: lowerer.lowerExprExpecting(arg, DYN),
|
||||
);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn,
|
||||
args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }],
|
||||
type: { kind: "promise", inner: member === "verify" ? BOOL : DYN },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `v instanceof CryptoKey` against the global. */
|
||||
export function lowerCryptoKeyInstanceof(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
): IrExpr | null {
|
||||
if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null;
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "webcrypto.isCryptoKey",
|
||||
args: [value],
|
||||
type: BOOL,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
|
||||
/** TypeScript reads of `crypto.subtle` and its members outside a direct
|
||||
* call: the global's dynamic object does not carry the lowered methods,
|
||||
* so a detached read is refused rather than answering a value without
|
||||
* them. JavaScript keeps the dynamic object's reads. */
|
||||
export function fenceWebCryptoMember(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
use: "read" | "call",
|
||||
): IrExpr | null {
|
||||
if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null;
|
||||
if (isJsSourceFile(access.getSourceFile())) return null;
|
||||
if (isSubtleReceiver(lowerer, access)) {
|
||||
lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT);
|
||||
}
|
||||
if (isSubtleReceiver(lowerer, access.expression)) {
|
||||
lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a checker type is one of the WebCrypto interfaces reached
|
||||
* through the crypto global's members (SubtleCrypto, CryptoKey and the
|
||||
* algorithm dictionaries). While `crypto` was typed `unknown` those member
|
||||
* reads were untyped, so JavaScript destructurings of them keep the
|
||||
* module-global path untyped bindings take. `Crypto` itself is excluded:
|
||||
* the global was `unknown`, which already mapped to dyn. */
|
||||
export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
symbol.name !== "Crypto" &&
|
||||
WEBCRYPTO_DYN_TYPES.has(symbol.name) &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
);
|
||||
}
|
||||
@@ -175,7 +175,7 @@ function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: strin
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getSymbol();
|
||||
if (sym?.name !== name) return false;
|
||||
return lowerer.checker.declarationsOf(sym).some((d) => {
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
let up: ts.Node | undefined = d.parent;
|
||||
|
||||
@@ -408,7 +408,15 @@ export function paramShapes(
|
||||
return { type: mapped, mode: "rest" };
|
||||
}
|
||||
if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped);
|
||||
if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) {
|
||||
// A checked-value slot holds undefined directly, so an omitted
|
||||
// argument needs no union arm (paramShape's declared-signature rule;
|
||||
// an erased type parameter and a class-implemented interface view
|
||||
// both map here).
|
||||
if (
|
||||
declParam.questionToken &&
|
||||
!lowerer.bareUndefinedArmedUnion(mapped) &&
|
||||
mapped.kind !== "dyn"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
declParam,
|
||||
@@ -903,8 +911,7 @@ export function lambdaSignature(
|
||||
const ctxSigs = ctxType
|
||||
? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType))
|
||||
: [];
|
||||
const ctxDecl =
|
||||
ctxSigs.length === 1 ? lowerer.checker.signatureDeclaration(ctxSigs[0]!) : undefined;
|
||||
const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined;
|
||||
const ctxRetRaw =
|
||||
ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
|
||||
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
|
||||
|
||||
@@ -37,7 +37,7 @@ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): b
|
||||
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
|
||||
if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : [];
|
||||
const decls = symbol ? symbol.declarations : [];
|
||||
return !decls.some(
|
||||
(d) =>
|
||||
!d.getSourceFile().isDeclarationFile &&
|
||||
@@ -349,7 +349,7 @@ function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean {
|
||||
/** A function declaration, or a variable initialized with a function or
|
||||
* arrow expression, that never reads `this`. */
|
||||
function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean {
|
||||
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
|
||||
const decls = sym ? sym.declarations : [];
|
||||
const fn = decls.find(ts.isFunctionDeclaration);
|
||||
if (fn !== undefined) return !functionUsesThis(fn);
|
||||
const v = decls.find(ts.isVariableDeclaration);
|
||||
|
||||
@@ -353,7 +353,7 @@ export function cjsExportValueByName(
|
||||
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
|
||||
const direct = lowerer.globalsBySymbol.get(sym);
|
||||
if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) };
|
||||
for (const d of lowerer.checker.declarationsOf(sym)) {
|
||||
for (const d of sym.declarations) {
|
||||
const byDecl = lowerer.globalsByDeclNode.get(d);
|
||||
if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) };
|
||||
// Alias plumbing: a shorthand/identifier table entry or a member
|
||||
|
||||
@@ -132,7 +132,7 @@ export function tscInteropBindingOf(
|
||||
): TscInteropBinding | null {
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined;
|
||||
const decl = sym ? sym.declarations.find(ts.isVariableDeclaration) : undefined;
|
||||
return decl ? tscInteropBindingOfDecl(lowerer, decl) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
@@ -389,13 +390,13 @@ export class ClassConstructionDispatch {
|
||||
if (!args) continue;
|
||||
const type: IrType = { kind: "classval", className: name };
|
||||
const callee = varRef("p.0", DYN, loc);
|
||||
const value: IrExpr = {
|
||||
const value = constructedValue(lowerer, info, {
|
||||
kind: "newValue",
|
||||
callee: { kind: "dynCheck", value: callee, type, loc },
|
||||
args,
|
||||
type: { kind: "object", className: name },
|
||||
loc,
|
||||
};
|
||||
});
|
||||
const body: IrStmt[] = [
|
||||
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -2883,7 +2884,7 @@ export class ClassDynamicDispatch {
|
||||
if (info.symbolMethods)
|
||||
for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]);
|
||||
for (const [symbol, name] of symbolEntries) {
|
||||
const declaration = lowerer.checker.valueDeclarationOf(symbol);
|
||||
const declaration = symbol.valueDeclaration;
|
||||
if (
|
||||
declaration &&
|
||||
ts.isVariableDeclaration(declaration) &&
|
||||
@@ -3164,13 +3165,13 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
if (!supported || !info.decl) continue;
|
||||
const cls = classValueRef(lowerer, info, info.decl);
|
||||
const result: IrExpr = {
|
||||
const result = constructedValue(lowerer, info, {
|
||||
kind: "new",
|
||||
className: name,
|
||||
args,
|
||||
type: { kind: "object", className: name },
|
||||
loc,
|
||||
};
|
||||
});
|
||||
this.constructDispatch.body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
|
||||
@@ -93,7 +93,7 @@ export function collectClassMethodMutations(
|
||||
value = unwrap(value);
|
||||
if (ts.isIdentifier(value)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(value);
|
||||
const decl = symbol && lowerer.checker.declarationsOf(symbol)[0];
|
||||
const decl = symbol && symbol.declarations[0];
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
@@ -162,7 +162,7 @@ export function collectClassMethodMutations(
|
||||
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
|
||||
if (!ts.isIdentifier(key)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(key);
|
||||
const decl = symbol && lowerer.checker.declarationsOf(symbol)[0];
|
||||
const decl = symbol && symbol.declarations[0];
|
||||
if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null;
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null;
|
||||
@@ -413,7 +413,7 @@ export function collectClassMethodMutations(
|
||||
const symbol = ts.isPropertyAccessExpression(access)
|
||||
? lowerer.checker.getSymbolAtLocation(access.name)
|
||||
: lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name);
|
||||
if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration))
|
||||
if (symbol && symbol.declarations.some(ts.isMethodDeclaration))
|
||||
lowerer.prototypeMethodAccesses.set(name, access);
|
||||
}
|
||||
lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]);
|
||||
@@ -480,8 +480,7 @@ export function collectClassMethodMutations(
|
||||
}
|
||||
const type = lowerer.checker.getDeclaredTypeOfSymbol(symbol);
|
||||
const property = lowerer.checker.getPropertyOfType(type, name);
|
||||
if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration))
|
||||
continue;
|
||||
if (!property || !property.declarations.some(ts.isMethodDeclaration)) continue;
|
||||
if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) {
|
||||
owners = null;
|
||||
break;
|
||||
|
||||
@@ -99,7 +99,7 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
if (!symbol) return false;
|
||||
const cached = lowerer.protocolOwnedBindings.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
const decl = symbol.valueDeclaration;
|
||||
let owned =
|
||||
decl !== undefined &&
|
||||
decl.getSourceFile() === id.getSourceFile() &&
|
||||
@@ -191,7 +191,7 @@ function protocolSignature(
|
||||
const recvTs = lowerer.typeOf(access.expression);
|
||||
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
|
||||
if (!property) return null;
|
||||
const declarations = lowerer.checker.declarationsOf(property);
|
||||
const declarations = property.declarations;
|
||||
if (
|
||||
declarations.length === 0 ||
|
||||
!declarations.every(
|
||||
@@ -200,7 +200,7 @@ function protocolSignature(
|
||||
)
|
||||
return null;
|
||||
const signature = lowerer.checker.getResolvedSignature(call);
|
||||
const declaration = signature && lowerer.checker.signatureDeclaration(signature);
|
||||
const declaration = signature && signature.declaration;
|
||||
if (
|
||||
!signature ||
|
||||
!declaration ||
|
||||
@@ -490,7 +490,7 @@ export function lowerProtocolPropertyRead(
|
||||
if (!lowerer.classImplementedProtocol(receiverType)) return null;
|
||||
const name = access.name.text;
|
||||
const property = lowerer.checker.getPropertyOfType(receiverType, name);
|
||||
const declarations = property ? lowerer.checker.declarationsOf(property) : [];
|
||||
const declarations = property ? property.declarations : [];
|
||||
if (declarations.length === 0 || !declarations.every((decl) => ts.isPropertySignature(decl)))
|
||||
return null;
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(access));
|
||||
|
||||
@@ -37,15 +37,13 @@ export class ClassProtocols {
|
||||
const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
|
||||
for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) {
|
||||
if (
|
||||
!this.checker
|
||||
.declarationsOf(property)
|
||||
.some(
|
||||
(member) =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
!member.modifiers?.nodes.some(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
||||
),
|
||||
)
|
||||
!property.declarations.some(
|
||||
(member) =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
!member.modifiers?.nodes.some(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
||||
),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
const entries = this.methods.get(property.name) ?? [];
|
||||
@@ -289,7 +287,7 @@ export class ClassProtocols {
|
||||
for (const part of parts) {
|
||||
if ((part.flags & ts.TypeFlags.TypeParameter) === 0) continue;
|
||||
const symbol = part.getSymbol();
|
||||
for (const declaration of symbol ? this.checker.declarationsOf(symbol) : [])
|
||||
for (const declaration of symbol ? symbol.declarations : [])
|
||||
if (ts.isTypeParameterDeclaration(declaration) && declaration.parent !== undefined)
|
||||
facts.genericObservers.add(declaration.parent);
|
||||
}
|
||||
@@ -501,7 +499,7 @@ export class ClassProtocols {
|
||||
if (context !== undefined && this.acceptsAnyValue(context)) return true;
|
||||
if (facts.genericObservers.size === 0) return false;
|
||||
const signature = this.checker.getResolvedSignature(arg.call);
|
||||
const declaration = signature && this.checker.signatureDeclaration(signature);
|
||||
const declaration = signature && signature.declaration;
|
||||
return (
|
||||
declaration !== undefined &&
|
||||
(facts.genericObservers.has(declaration) ||
|
||||
@@ -520,13 +518,10 @@ export class ClassProtocols {
|
||||
.some(
|
||||
(property) =>
|
||||
names.has(property.name) &&
|
||||
this.checker
|
||||
.declarationsOf(property)
|
||||
.some(
|
||||
(declaration) =>
|
||||
ts.isGetAccessorDeclaration(declaration) ||
|
||||
ts.isSetAccessorDeclaration(declaration),
|
||||
),
|
||||
property.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,9 +575,7 @@ export class ClassProtocols {
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
|
||||
this.checker
|
||||
.declarationsOf(symbol)
|
||||
.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
|
||||
symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
|
||||
// `typeof C` (the class value) shares the class symbol.
|
||||
this.checker.getConstructSignatures(type).length === 0
|
||||
);
|
||||
@@ -603,13 +596,10 @@ export class ClassProtocols {
|
||||
// A program interface a class matches by its methods (protocol below).
|
||||
return (
|
||||
this.methods.size > 0 &&
|
||||
this.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!declaration.getSourceFile().isDeclarationFile,
|
||||
) &&
|
||||
symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
|
||||
) &&
|
||||
this.protocol(type)
|
||||
);
|
||||
}
|
||||
@@ -624,7 +614,7 @@ export class ClassProtocols {
|
||||
if (this.checker.getConstructSignatures(type).length > 0) return false;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) !== 0) return false;
|
||||
const declarations = this.checker.declarationsOf(symbol);
|
||||
const declarations = symbol.declarations;
|
||||
return (
|
||||
declarations.length > 0 &&
|
||||
declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile)
|
||||
@@ -712,13 +702,10 @@ export class ClassProtocols {
|
||||
if (symbol && protocols.has(symbol)) return true;
|
||||
if (
|
||||
!symbol ||
|
||||
!this.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!declaration.getSourceFile().isDeclarationFile,
|
||||
)
|
||||
!symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
for (const property of properties) {
|
||||
|
||||
@@ -555,7 +555,7 @@ function prototypeProtectedKeys(
|
||||
} else if (name.startsWith("%symbol:")) {
|
||||
const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : [];
|
||||
const entry = entries.find(([, field]) => field === name);
|
||||
const declaration = entry ? lowerer.checker.valueDeclarationOf(entry[0]) : undefined;
|
||||
const declaration = entry ? entry[0].valueDeclaration : undefined;
|
||||
if (
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
|
||||
@@ -270,8 +270,8 @@ function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string):
|
||||
}
|
||||
|
||||
/** ToString(ToPrimitive(value, hint)) of a union whose arms are program
|
||||
* class instances and primitives (`Box | undefined`): each arm converts as
|
||||
* it would alone. Null for other unions. */
|
||||
* class instances, URLs and primitives (`Box | undefined`, `string | URL`):
|
||||
* each arm converts as it would alone. Null for other unions. */
|
||||
export function classUnionToString(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
@@ -281,9 +281,11 @@ export function classUnionToString(
|
||||
if (value.type.kind !== "union") return null;
|
||||
const unionId = value.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms;
|
||||
if (!arms || !arms.some((arm) => arm.kind === "object")) return null;
|
||||
if (!arms || !arms.some((arm) => arm.kind === "object" || arm.kind === "url")) return null;
|
||||
const convertible = (arm: IrType): boolean => {
|
||||
if (PRIMITIVE_KINDS.has(arm.kind)) return true;
|
||||
// A URL arm converts through its href, like String(url).
|
||||
if (arm.kind === "url") return true;
|
||||
if (arm.kind !== "object") return false;
|
||||
const info = lowerer.classes.get(arm.className);
|
||||
return info !== undefined && plainProgramClass(info);
|
||||
|
||||
@@ -1,8 +1,115 @@
|
||||
import { BOOL, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** The classes a narrowed read of a `staticClass` value can hold when the
|
||||
* checker typed it as the subclass `narrowed`, as a class union, or null
|
||||
* when `narrowed` is the only one.
|
||||
*
|
||||
* TypeScript reduces a union of type-parameter intersections by structural
|
||||
* subtyping. With `<T extends Node>`, `node instanceof Identifier || node
|
||||
* instanceof PrivateIdentifier` narrows `node` to `T & PrivateIdentifier`
|
||||
* when Identifier has every member PrivateIdentifier has, so the narrowed
|
||||
* read can hold an Identifier. Class identity is nominal in compiled code:
|
||||
* such a read holds the narrowed class or any class under `staticClass`
|
||||
* that is assignable to it without extending it. A narrowing outside a
|
||||
* type-parameter intersection keeps every class it names. */
|
||||
export function structuralNarrowing(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
staticClass: string,
|
||||
narrowed: IrType & { kind: "object" },
|
||||
): (IrType & { kind: "union" }) | null {
|
||||
if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null;
|
||||
const key = `${staticClass}:${narrowed.className}`;
|
||||
let peers = lowerer.structuralClassPeers.get(key);
|
||||
if (peers === undefined) {
|
||||
peers = assignableClassPeers(lowerer, staticClass, narrowed.className);
|
||||
lowerer.structuralClassPeers.set(key, peers);
|
||||
}
|
||||
if (peers.length === 0) return null;
|
||||
const arms: IrType[] = [
|
||||
narrowed,
|
||||
...peers.map((className) => ({ kind: "object" as const, className })),
|
||||
];
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: lowerer.unions.intern(arms) };
|
||||
}
|
||||
|
||||
/** The type a member access dispatches on when its receiver `node` is a
|
||||
* narrowed read of a type-parameter intersection, or null to use the
|
||||
* checker's type: the class union structuralNarrowing reads, or the
|
||||
* receiver's own class when this instantiation's class is unrelated to the
|
||||
* narrowed one but assignable to it (with `T = Identifier`, the reduced
|
||||
* `T & PrivateIdentifier` still holds an Identifier). */
|
||||
export function structuralReadType(lowerer: Lowerer, node: ts.Expression): IrType | null {
|
||||
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null;
|
||||
if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null;
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowed?.kind !== "object") return null;
|
||||
const stored = ts.isIdentifier(node) ? lowerer.peekLocal(node)?.type : undefined;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(node) ? node : node.name);
|
||||
const declared =
|
||||
stored ?? (symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null);
|
||||
if (declared?.kind !== "object" || declared.className === narrowed.className) return null;
|
||||
if (lowerer.isSubclassOf(narrowed.className, declared.className))
|
||||
return structuralNarrowing(lowerer, node, declared.className, narrowed);
|
||||
if (lowerer.isSubclassOf(declared.className, narrowed.className)) return null;
|
||||
const declaredType = classInstanceType(lowerer, declared.className);
|
||||
const narrowedType = classInstanceType(lowerer, narrowed.className);
|
||||
return declaredType !== null &&
|
||||
narrowedType !== null &&
|
||||
lowerer.checker.isTypeAssignableTo(declaredType, narrowedType)
|
||||
? declared
|
||||
: null;
|
||||
}
|
||||
|
||||
function classInstanceType(lowerer: Lowerer, className: string): ts.Type | null {
|
||||
const decl = lowerer.classes.get(className)?.decl;
|
||||
if (!decl?.name || decl.typeParameters?.nodes.length) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
return symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null;
|
||||
}
|
||||
|
||||
function intersectsTypeParameter(type: ts.Type): boolean {
|
||||
try {
|
||||
if (type.isUnionType()) return ts.constituentTypes(type).some(intersectsTypeParameter);
|
||||
return (
|
||||
(type.flags & ts.TypeFlags.Intersection) !== 0 &&
|
||||
ts.constituentTypes(type).some((part) => (part.flags & ts.TypeFlags.TypeParameter) !== 0)
|
||||
);
|
||||
} catch (e) {
|
||||
if (ts.isUnreadableConstituentsPanic(e)) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** The outermost classes under `staticClass`, other than `target` and its
|
||||
* subclasses, whose instance type is assignable to `target`'s. */
|
||||
function assignableClassPeers(lowerer: Lowerer, staticClass: string, target: string): string[] {
|
||||
const targetType = classInstanceType(lowerer, target);
|
||||
if (targetType === null) return [];
|
||||
const assignable: string[] = [];
|
||||
for (const className of lowerer.classes.keys()) {
|
||||
if (
|
||||
className === target ||
|
||||
!lowerer.isSubclassOf(className, staticClass) ||
|
||||
lowerer.isSubclassOf(className, target)
|
||||
)
|
||||
continue;
|
||||
const type = classInstanceType(lowerer, className);
|
||||
if (type !== null && lowerer.checker.isTypeAssignableTo(type, targetType))
|
||||
assignable.push(className);
|
||||
}
|
||||
// A subclass of an assignable class is one of its instances already.
|
||||
return assignable.filter(
|
||||
(className) =>
|
||||
!assignable.some((other) => other !== className && lowerer.isSubclassOf(className, other)),
|
||||
);
|
||||
}
|
||||
|
||||
/** A base-class slot stays one pointer after a guard selects several
|
||||
* subclasses. Recover the tagged view from runtime membership without
|
||||
* copying the object or guessing which of the remaining layouts it has. */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { planRecordUnionWrap } from "../union-retag.js";
|
||||
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import type { IrType } from "../../../ir/ir.js";
|
||||
import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
@@ -78,6 +78,24 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
});
|
||||
if (absentOnly.length === 1) selected = absentOnly;
|
||||
}
|
||||
// A record that fits several arms of a discriminated union selects one
|
||||
// by its discriminant at run time. Without this, an arm that a nested
|
||||
// field could reach only ambiguously dropped out of the candidates, and
|
||||
// an arm reached by dropping fields could win alone.
|
||||
if (selected.length !== 1 && src.kind === "record") {
|
||||
const source = lowerer.shapes.get(src.shapeId);
|
||||
if (
|
||||
source &&
|
||||
planRecordUnionLift(
|
||||
source,
|
||||
def,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(from, to) => lowerer.widthLiftPlan(from, to),
|
||||
)
|
||||
) {
|
||||
return { how: "discriminantRoute" };
|
||||
}
|
||||
}
|
||||
if (selected.length !== 1) return null;
|
||||
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { constructedValue } from "../constructor-returns.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import type { FieldLift } from "./structural-plans.js";
|
||||
import { buildArrayConversion } from "./builders.js";
|
||||
@@ -106,6 +107,16 @@ export function applyWidthLift(
|
||||
);
|
||||
return { kind: "call", callee: helper, args: [value], type: dst, loc };
|
||||
}
|
||||
case "discriminantRoute": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "record")
|
||||
throw new InternalCompilerError("lowerer bug: record discriminant route shape");
|
||||
const helper = lowerer.recordUnionRouteHelper(value.type, dst.unionId, loc);
|
||||
if (!helper)
|
||||
throw new InternalCompilerError(
|
||||
"lowerer bug: planned record discriminant route failed to intern",
|
||||
);
|
||||
return { kind: "call", callee: helper, args: [value], type: dst, loc };
|
||||
}
|
||||
case "retag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: retag lift shape");
|
||||
@@ -524,7 +535,13 @@ export function recordClassWidthHelper(
|
||||
params: [{ localId: "r.0", name: "r", type: fromT }],
|
||||
returnType: toT,
|
||||
locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }],
|
||||
body: [{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: constructedValue(lowerer, info, { kind: "new", className, args, type: toT, loc }),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
@@ -603,6 +620,10 @@ function buildRecordProjection(
|
||||
const localId = parameter + ".0";
|
||||
const receiver = varRef(localId, source, loc);
|
||||
const result: IrType = { kind: "record", shapeId: target.id };
|
||||
// A record source's copy keeps the source's key order; a class
|
||||
// instance's fields keep today's table.
|
||||
const copyOf = (node: IrExpr): IrExpr =>
|
||||
source.kind === "record" ? lowerer.keyOrder.copy(node, source.shapeId) : node;
|
||||
return {
|
||||
name,
|
||||
params: [{ localId, name: parameter, type: source }],
|
||||
@@ -611,7 +632,7 @@ function buildRecordProjection(
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
value: copyOf({
|
||||
kind: "recordLit",
|
||||
fields: target.fields.map((field) => {
|
||||
const conversion = plan.get(field.name)!;
|
||||
@@ -661,7 +682,7 @@ function buildRecordProjection(
|
||||
}),
|
||||
type: result,
|
||||
loc,
|
||||
},
|
||||
}),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { nodeThrowExpr } from "../../../ir/build.js";
|
||||
import {
|
||||
planUnionRetag,
|
||||
buildUnionRetag,
|
||||
planRecordUnionLift,
|
||||
planRecordUnionWrap,
|
||||
buildRecordUnionWrap,
|
||||
} from "../union-retag.js";
|
||||
@@ -284,6 +285,38 @@ export function recordUnionWrapHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Interned `%record.route.<n>(r)`: a record that fits several arms of a
|
||||
* discriminated union, converted into the arm its discriminant names
|
||||
* (planRecordUnionLift). The width plan's `discriminantRoute` lift. */
|
||||
export function recordUnionRouteHelper(
|
||||
lowerer: Lowerer,
|
||||
source: IrType & { kind: "record" },
|
||||
toId: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
const shape = lowerer.shapes.get(source.shapeId);
|
||||
const to = lowerer.unions.get(toId);
|
||||
if (!shape || !to) return null;
|
||||
const key = `recordRoute:${source.shapeId}:${toId}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const plan = planRecordUnionLift(
|
||||
shape,
|
||||
to,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(from, dst) => lowerer.widthLiftPlan(from, dst),
|
||||
);
|
||||
if (!plan) return null;
|
||||
const name = `%record.route.${lowerer.coercions.retags.size}`;
|
||||
lowerer.coercions.retags.set(key, name);
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) =>
|
||||
lowerer.applyWidthLift(lift, value, dst, loc),
|
||||
),
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
export function unionRetagHelper(
|
||||
lowerer: Lowerer,
|
||||
fromId: string,
|
||||
|
||||
@@ -13,12 +13,7 @@ export function concreteCollectionNarrow(
|
||||
const symbol = narrowed.getSymbol();
|
||||
const name = symbol?.name;
|
||||
if (name !== "Map" && name !== "Set") return null;
|
||||
if (
|
||||
!symbol ||
|
||||
!lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
|
||||
)
|
||||
if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())))
|
||||
return null;
|
||||
const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference);
|
||||
if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null;
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
/** Constructors that `return` a value.
|
||||
*
|
||||
* JavaScript's `new C()` answers the constructor's returned value when it
|
||||
* is an object (functions included) and the constructed `this` otherwise.
|
||||
* A primitive or bare return therefore keeps the ordinary lowering, which
|
||||
* evaluates the operand and drops it.
|
||||
*
|
||||
* A return whose value is an instance of the constructor's own class keeps
|
||||
* the class representation (`if (Single.instance) return
|
||||
* Single.instance;`). The return stores that instance in a per-class
|
||||
* result slot and every direct `new C()` takes it from there. Any other
|
||||
* object has no representation as a `C` instance and refuses at the return
|
||||
* statement, deferred to run time in JavaScript like other statement
|
||||
* fences. A JavaScript value that is an object only at run time (`any`)
|
||||
* is tested when it returns: a primitive answers the constructed
|
||||
* instance, an instance of the class takes the result slot, and any other
|
||||
* object refuses.
|
||||
*
|
||||
* `super()` from a subclass refuses: Node would rebind the subclass's
|
||||
* `this` to the returned instance. In JavaScript the refusal is a run-time
|
||||
* test after the call: an instance other than the subclass's own `this`
|
||||
* refuses, and the constructed instance itself (`return this.set(r, g,
|
||||
* b)`, where `set` answers `this`) continues. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrGlobal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
typeKey,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
type ReturnKind = "instance" | "object" | "mixed";
|
||||
|
||||
export interface ConstructorReturnPlan {
|
||||
/** Returns whose value may be an object, by kind. Primitive and `this`
|
||||
* returns are absent: they keep the ordinary lowering. */
|
||||
returns: Map<ts.ReturnStatement, ReturnKind>;
|
||||
/** The result slot (`C | undefined`), present when some return yields an
|
||||
* instance of the class itself. */
|
||||
slot: IrGlobal | null;
|
||||
}
|
||||
|
||||
const PRIMITIVE_FLAGS =
|
||||
ts.TypeFlags.Primitive | ts.TypeFlags.EnumLike | ts.TypeFlags.Void | ts.TypeFlags.Never;
|
||||
|
||||
/** Types whose values may be primitives or objects: open types, type
|
||||
* variables and intersections (a branded `string & { tag }` is a string). */
|
||||
const OPEN_FLAGS =
|
||||
ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Intersection;
|
||||
|
||||
function unparenthesized(node: ts.Expression): ts.Expression {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Whether a value of this checker type can be an object: "never",
|
||||
* "always", or "maybe" (open types and primitive/object unions). */
|
||||
function objectness(type: ts.Type): "never" | "always" | "maybe" {
|
||||
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
let primitive = false;
|
||||
let object = false;
|
||||
for (const part of parts) {
|
||||
if ((part.flags & OPEN_FLAGS) !== 0) return "maybe";
|
||||
if ((part.flags & PRIMITIVE_FLAGS) !== 0) primitive = true;
|
||||
else object = true;
|
||||
}
|
||||
if (!object) return "never";
|
||||
return primitive ? "maybe" : "always";
|
||||
}
|
||||
|
||||
/** The constructor's own `return <expr>` statements: nested functions,
|
||||
* arrows and classes have their own returns. */
|
||||
function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] {
|
||||
const out: ts.ReturnStatement[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node);
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
ctor.body?.forEachChild(visit);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A return inside a `try` with a `finally` block: the finally block runs
|
||||
* after the result is stored and may throw past it. */
|
||||
function insideFinallyTry(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
|
||||
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
|
||||
if (ts.isTryStatement(p) && p.finallyBlock !== undefined && !nodeWithin(node, p.finallyBlock))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function nodeWithin(node: ts.Node, ancestor: ts.Node): boolean {
|
||||
return node.pos >= ancestor.pos && node.end <= ancestor.end;
|
||||
}
|
||||
|
||||
function instanceType(info: ClassInfo): IrType {
|
||||
return { kind: "object", className: info.def.name };
|
||||
}
|
||||
|
||||
function isInstanceOf(lowerer: Lowerer, info: ClassInfo, type: IrType | null): boolean {
|
||||
if (type === null) return false;
|
||||
if (type.kind === "object") return type.className === info.def.name;
|
||||
if (type.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.length === 2 &&
|
||||
arms.some((arm) => arm.kind === "object" && arm.className === info.def.name) &&
|
||||
arms.some((arm) => arm.kind === "undefinedT")
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether an expression always yields the receiver itself: `this`, or a
|
||||
* call `this.m(...)` / `super.m(...)` whose every implementation the
|
||||
* receiver can reach (the statically resolved method and, for `this`
|
||||
* calls, each subclass override) returns only `this`, a primitive, or
|
||||
* nothing (`return this.set(r, g, b)` with a chainable setter). Such a
|
||||
* return answers the constructed instance. Methods already being checked
|
||||
* count as returning `this` (their other returns decide). */
|
||||
function returnsThis(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
node: ts.Expression,
|
||||
visiting: Set<ts.Node> = new Set(),
|
||||
): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e))
|
||||
e = e.expression;
|
||||
if (e.kind === ts.SyntaxKind.ThisKeyword) return true;
|
||||
if (!ts.isCallExpression(e)) return false;
|
||||
const callee = unparenthesized(e.expression);
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(callee) ||
|
||||
callee.questionDotToken !== undefined ||
|
||||
!ts.isIdentifier(callee.name)
|
||||
)
|
||||
return false;
|
||||
const receiver = unparenthesized(callee.expression).kind;
|
||||
if (receiver !== ts.SyntaxKind.ThisKeyword && receiver !== ts.SyntaxKind.SuperKeyword)
|
||||
return false;
|
||||
const name = callee.name.text;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(callee.name);
|
||||
if (symbol === undefined) return false;
|
||||
const methods: ts.Node[] = [...symbol.declarations];
|
||||
if (receiver === ts.SyntaxKind.ThisKeyword) {
|
||||
const pending = [...info.subclasses];
|
||||
while (pending.length > 0) {
|
||||
const sub = pending.pop()!;
|
||||
pending.push(...sub.subclasses);
|
||||
for (const member of sub.decl?.members.nodes ?? []) {
|
||||
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== name)
|
||||
continue;
|
||||
if (!ts.isMethodDeclaration(member)) return false;
|
||||
methods.push(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (methods.length === 0) return false;
|
||||
for (const method of methods) {
|
||||
if (!ts.isMethodDeclaration(method) || method.body === undefined) return false;
|
||||
if (visiting.has(method)) continue;
|
||||
visiting.add(method);
|
||||
for (const ret of ownReturns(method.body)) {
|
||||
if (ret.expression === undefined) continue;
|
||||
if (objectness(lowerer.typeOf(ret.expression)) === "never") continue;
|
||||
if (!returnsThis(lowerer, info, ret.expression, visiting)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A function body's own return statements (nested functions, arrows and
|
||||
* classes have their own). */
|
||||
function ownReturns(body: ts.Node): ts.ReturnStatement[] {
|
||||
const out: ts.ReturnStatement[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
|
||||
if (ts.isReturnStatement(node)) out.push(node);
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function constructorReturnPlan(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
): ConstructorReturnPlan | null {
|
||||
const byClass = lowerer.constructorReturnPlans;
|
||||
const known = byClass.get(info.def.name);
|
||||
if (known !== undefined) return known;
|
||||
const ctor = info.ctor;
|
||||
let plan: ConstructorReturnPlan | null = null;
|
||||
if (ctor?.body !== undefined && info.decl !== null) {
|
||||
const returns = new Map<ts.ReturnStatement, ReturnKind>();
|
||||
for (const stmt of ownValueReturns(ctor)) {
|
||||
const value = unparenthesized(stmt.expression!);
|
||||
if (returnsThis(lowerer, info, value)) continue;
|
||||
const type = lowerer.typeOf(value);
|
||||
const shape = objectness(type);
|
||||
if (shape === "never") continue;
|
||||
// Local classes construct through their own environment, which has
|
||||
// no result slot.
|
||||
const instance =
|
||||
info.localClass === undefined &&
|
||||
!insideFinallyTry(stmt, ctor) &&
|
||||
isInstanceOf(lowerer, info, lowerer.mapTypeOf(type));
|
||||
returns.set(stmt, instance ? "instance" : shape === "always" ? "object" : "mixed");
|
||||
}
|
||||
if (returns.size > 0) {
|
||||
let slot: IrGlobal | null = null;
|
||||
// A JavaScript value typed only at run time may hold an instance of
|
||||
// the class too (`if (C.instance) return C.instance;`).
|
||||
const runtimeInstance =
|
||||
info.localClass === undefined &&
|
||||
isJsSourceFile(ctor.getSourceFile()) &&
|
||||
[...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor));
|
||||
if ([...returns.values()].includes("instance") || runtimeInstance) {
|
||||
slot = {
|
||||
id: `%g.${info.def.name}.%constructorResult`,
|
||||
name: `${info.def.name}.%constructorResult`,
|
||||
type: lowerer.withUndefinedArm(instanceType(info)),
|
||||
mutable: true,
|
||||
};
|
||||
lowerer.globalsList.push(slot);
|
||||
}
|
||||
plan = { returns, slot };
|
||||
}
|
||||
}
|
||||
byClass.set(info.def.name, plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
function foreignObjectFeature(info: ClassInfo): string {
|
||||
const display = info.def.name.replace(/^%|^%m\d+\./, "");
|
||||
return `returning an object other than an instance of the class from a constructor (Node's 'new ${display}()' answers the returned object, which has no representation as a '${display}' instance)`;
|
||||
}
|
||||
|
||||
/** `return <expr>` inside a class constructor whose value may be an
|
||||
* object. Null when the statement keeps the ordinary lowering. */
|
||||
export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
|
||||
const info = lowerer.currentClass;
|
||||
if (info === null || info.ctor === null || stmt.expression === undefined) return null;
|
||||
let fn: ts.Node | undefined = stmt.parent;
|
||||
while (fn !== undefined && !ts.isFunctionLike(fn) && !ts.isClassLikeDeclaration(fn))
|
||||
fn = fn.parent;
|
||||
if (fn !== info.ctor) return null;
|
||||
const plan = constructorReturnPlan(lowerer, info);
|
||||
const kind = plan?.returns.get(stmt);
|
||||
if (plan === null || kind === undefined) return null;
|
||||
const loc = locOf(stmt);
|
||||
if (kind === "instance") {
|
||||
const value = lowerer.coerceInto(
|
||||
stmt.expression,
|
||||
lowerer.lowerExpr(stmt.expression),
|
||||
plan.slot!.type,
|
||||
);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "assign", localId: plan.slot!.id, value, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const feature = foreignObjectFeature(info);
|
||||
if (kind === "object" || !isJsSourceFile(stmt.getSourceFile()))
|
||||
lowerer.unsupported("SC1090", stmt, feature);
|
||||
// A JavaScript value that is an object only sometimes: a primitive
|
||||
// still answers the constructed instance, an object refuses.
|
||||
const value = lowerer.coerceToExpected(lowerer.lowerExpr(stmt.expression), DYN);
|
||||
if (value.type.kind !== "dyn") lowerer.unsupported("SC1090", stmt, feature);
|
||||
const held = lowerer.declareHiddenLocal("%returned", DYN);
|
||||
const read = varRef(held.id, DYN, loc);
|
||||
const typeOf: IrExpr = { kind: "libCall", fn: "dyn.typeof", args: [read], type: STRING, loc };
|
||||
const typeIs = (name: string): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: typeOf,
|
||||
right: { kind: "strLit", value: name, type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const diag = unsupportedDiag("SC1090", loc, feature);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const sf = stmt.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1;
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: held.id, init: value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
// null answers "object" and is falsy; every object is truthy.
|
||||
cond: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: typeIs("function"),
|
||||
right: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: typeIs("object"),
|
||||
right: lowerer.ensureBool(read, stmt.expression),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
...(plan.slot !== null && !insideFinallyTry(stmt, info.ctor)
|
||||
? [
|
||||
{
|
||||
kind: "if" as const,
|
||||
cond: {
|
||||
kind: "libCall" as const,
|
||||
fn: "dyn.typedRefIs" as const,
|
||||
args: [
|
||||
read,
|
||||
{
|
||||
kind: "strLit" as const,
|
||||
value: typeKey(instanceType(info)),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "assign" as const,
|
||||
localId: plan.slot.id,
|
||||
value: lowerer.coerceToExpected(
|
||||
lowerer.coerceToExpected(read, instanceType(info)),
|
||||
plan.slot.type,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
{ kind: "return" as const, value: null, loc },
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: diag.code,
|
||||
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The value of a construction of exactly class `C`: the constructed
|
||||
* instance, or the instance the constructor returned. Unchanged for
|
||||
* classes without a result slot. The slot is read and cleared by a
|
||||
* per-class helper, so every construction site (direct `new`, class
|
||||
* values, the checked dynamic dispatchers) shares one protocol. */
|
||||
export function constructedValue(lowerer: Lowerer, info: ClassInfo, constructed: IrExpr): IrExpr {
|
||||
const slot = constructorReturnPlan(lowerer, info)?.slot;
|
||||
if (slot === null || slot === undefined) return constructed;
|
||||
const loc = constructed.loc;
|
||||
const type = instanceType(info);
|
||||
const name = `%${info.def.name}.%constructed`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const instance = { id: "instance.0", name: "instance", type, mutable: false };
|
||||
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: instance.id, name: instance.name, type }],
|
||||
returnType: type,
|
||||
locals: [instance, returned],
|
||||
body: [
|
||||
{ kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc },
|
||||
{ kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "orDefault",
|
||||
left: varRef(returned.id, slot.type, loc),
|
||||
right: varRef(instance.id, type, loc),
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [constructed], type, loc };
|
||||
}
|
||||
|
||||
/** A construction through a class value whose static class is `info`: the
|
||||
* value holds `info` itself or a descendant. Descendants of a class with a
|
||||
* result slot refuse at their `super()`, so only `info`'s own slot can
|
||||
* apply; a descendant with its own slot has no static home here. */
|
||||
export function constructedFromValue(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
constructed: IrExpr,
|
||||
blame: ts.Node,
|
||||
): IrExpr {
|
||||
const pending = [...info.subclasses];
|
||||
while (pending.length > 0) {
|
||||
const sub = pending.pop()!;
|
||||
pending.push(...sub.subclasses);
|
||||
const plan = constructorReturnPlan(lowerer, sub);
|
||||
if (plan === null || plan.slot === null) continue;
|
||||
const display = sub.def.name.replace(/^%|^%m\d+\./, "");
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
blame,
|
||||
`constructing through a class value that may hold '${display}', whose constructor can return an existing instance`,
|
||||
);
|
||||
}
|
||||
return constructedValue(lowerer, info, constructed);
|
||||
}
|
||||
|
||||
/** `super()` into a class with a result slot, from the constructor of
|
||||
* class `sub`: `call` runs the base constructor over `self` (the
|
||||
* subclass's `this`, upcast to the base). Node rebinds the subclass's
|
||||
* `this` to an instance the base constructor returns, which has no
|
||||
* representation. A TypeScript subclass refuses. In JavaScript the base
|
||||
* stores the instance it returns in its slot, and a per-subclass helper
|
||||
* reads and clears the slot after the call: an instance other than `self`
|
||||
* refuses at run time. Unchanged for bases without a result slot. */
|
||||
export function superIntoResultSlot(
|
||||
lowerer: Lowerer,
|
||||
base: ClassInfo,
|
||||
sub: ClassInfo,
|
||||
call: IrStmt,
|
||||
self: IrExpr,
|
||||
blame: ts.Node,
|
||||
): IrStmt {
|
||||
const plan = constructorReturnPlan(lowerer, base);
|
||||
if (plan === null || plan.slot === null) return call;
|
||||
const slot = plan.slot;
|
||||
const display = base.def.name.replace(/^%|^%m\d+\./, "");
|
||||
const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`;
|
||||
if (!isJsSourceFile(blame.getSourceFile())) {
|
||||
lowerer.unsupported("SC1090", blame, feature);
|
||||
return call;
|
||||
}
|
||||
const loc = locOf(blame);
|
||||
const type = instanceType(base);
|
||||
const name = `%${sub.def.name}.%superResult`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const diag = unsupportedDiag("SC1090", loc, feature);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1;
|
||||
const instance = { id: "instance.0", name: "instance", type, mutable: false };
|
||||
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: instance.id, name: instance.name, type }],
|
||||
returnType: VOID,
|
||||
locals: [instance, returned],
|
||||
body: [
|
||||
{ kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc },
|
||||
{ kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "!==",
|
||||
left: {
|
||||
kind: "orDefault",
|
||||
left: varRef(returned.id, slot.type, loc),
|
||||
right: varRef(instance.id, type, loc),
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
right: varRef(instance.id, type, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: diag.code,
|
||||
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const check: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "call", callee: name, args: [self], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
return { kind: "block", body: [call, check], loc };
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
arrayOf,
|
||||
bytesOf,
|
||||
funcOf,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
@@ -554,9 +555,9 @@ export function lowerBytesMethodCall(
|
||||
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
nameSym !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(nameSym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
|
||||
nameSym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
|
||||
)
|
||||
);
|
||||
})();
|
||||
const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice";
|
||||
@@ -580,13 +581,11 @@ export function lowerBytesMethodCall(
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration.parent) &&
|
||||
declaration.parent.name.text === "Buffer",
|
||||
)
|
||||
symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration.parent) &&
|
||||
declaration.parent.name.text === "Buffer",
|
||||
)
|
||||
);
|
||||
})()
|
||||
) {
|
||||
@@ -646,9 +645,9 @@ export function lowerBytesMethodCall(
|
||||
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
nameSym !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(nameSym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
|
||||
nameSym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
|
||||
)
|
||||
);
|
||||
})();
|
||||
if (receiverIr.elem !== "u8" || !declaredOnBuffer) {
|
||||
@@ -724,9 +723,9 @@ export function lowerBytesMethodCall(
|
||||
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
nameSym !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(nameSym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
|
||||
nameSym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
|
||||
)
|
||||
);
|
||||
})();
|
||||
if (declOnBuffer && receiverIr.elem === "u8") {
|
||||
@@ -1273,16 +1272,14 @@ function knownBufferProducer(
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
if (!symbol || seen.has(symbol)) return false;
|
||||
seen.add(symbol);
|
||||
return lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(decl) =>
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
||||
decl.initializer !== undefined &&
|
||||
knownBufferProducer(lowerer, decl.initializer, seen),
|
||||
);
|
||||
return symbol.declarations.some(
|
||||
(decl) =>
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
||||
decl.initializer !== undefined &&
|
||||
knownBufferProducer(lowerer, decl.initializer, seen),
|
||||
);
|
||||
}
|
||||
if (!ts.isCallExpression(node)) return false;
|
||||
const callee = node.expression;
|
||||
@@ -1429,7 +1426,14 @@ function lowerBufferStaticValue(
|
||||
}
|
||||
return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc };
|
||||
}
|
||||
if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") {
|
||||
// A checker type with no mapping (pako's conditional `O extends
|
||||
// { toText: true } ? string : Uint8Array` result) whose lowered
|
||||
// value is a u8 view copies like the typed form.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
((srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
|
||||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8"))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.poolFromBytes",
|
||||
@@ -1447,6 +1451,34 @@ function lowerBufferStaticValue(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A union of strings, u8 views and undefined/null (a JS-inferred
|
||||
// npm result such as pako's deflate): the checked form dispatches
|
||||
// on the runtime kind exactly as for an untyped JS value, Node's
|
||||
// TypeError for the unit arms included.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
value.type.kind === "union" &&
|
||||
lowerer.dynConvertible(value.type) &&
|
||||
lowerer.unions
|
||||
.get(value.type.unionId)
|
||||
?.arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "string" ||
|
||||
(arm.kind === "bytes" && arm.elem === "u8") ||
|
||||
isUnitType(arm),
|
||||
) === true
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.poolFromDyn",
|
||||
args: [
|
||||
lowerer.coerceToExpected(value, DYN),
|
||||
{ kind: "strLit", value: "utf8", type: STRING, loc },
|
||||
],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.noLowering(
|
||||
@@ -1659,6 +1691,12 @@ function lowerBufferStaticValue(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A typed view of unknown provenance (a Uint8Array parameter,
|
||||
// safe-buffer's Buffer.from result): Buffer is a brand of the view,
|
||||
// which the runtime keeps, so the test reads it.
|
||||
if (v.type.kind === "bytes") {
|
||||
return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`,
|
||||
args[0]!,
|
||||
|
||||
@@ -153,7 +153,7 @@ export function lowerForOfSearchParams(
|
||||
}
|
||||
: singleRead();
|
||||
if (ts.isIdentifier(decl.name)) {
|
||||
const varTarget = forOfVarTarget(lowerer, decl);
|
||||
const varTarget = forOfVarTarget(lowerer, decl, elemT!);
|
||||
if (varTarget) {
|
||||
// `for (var x of ...)`: one function-scoped binding, assigned
|
||||
// per pass — closures made in the loop share it, and the value
|
||||
@@ -357,7 +357,7 @@ export function lowerForOfArrayIter(
|
||||
}
|
||||
: keyRead();
|
||||
if (ts.isIdentifier(decl.name)) {
|
||||
const varTarget = forOfVarTarget(lowerer, decl);
|
||||
const varTarget = forOfVarTarget(lowerer, decl, declT!);
|
||||
if (varTarget) {
|
||||
// `for (var x of ...)`: one function-scoped binding, assigned
|
||||
// per pass — closures made in the loop share it, and the value
|
||||
@@ -650,7 +650,7 @@ function lowerForOfMapOrSet(
|
||||
}
|
||||
: singleRead();
|
||||
if (ts.isIdentifier(decl!.name)) {
|
||||
const varTarget = forOfVarTarget(lowerer, decl!);
|
||||
const varTarget = forOfVarTarget(lowerer, decl!, elemT!);
|
||||
if (varTarget) {
|
||||
// `for (var x of ...)`: one function-scoped binding, assigned
|
||||
// per pass — closures made in the loop share it, and the value
|
||||
|
||||
@@ -810,6 +810,7 @@ export function lowerRecordOvfCaptureHelper(
|
||||
// Every source declared field must flow into the keyed-write slot —
|
||||
// in DECLARATION order (insertion order below; declaredOrder omits
|
||||
// internal '%'-fields, which never enter the copy).
|
||||
lowerer.keyOrder.observed.add(from.id);
|
||||
const orderedFields = from.declaredOrder
|
||||
? from.declaredOrder.flatMap((n) => {
|
||||
const f = from.fields.find((x) => x.name === n);
|
||||
@@ -1097,6 +1098,7 @@ export function lowerObjectAssignIndexShape(
|
||||
const from = lowerer.shapes.get(srcIr.shapeId);
|
||||
if (!from || from.tuple) return null;
|
||||
if (from.fields.some((f) => f.name.startsWith("%"))) return null;
|
||||
lowerer.keyOrder.observed.add(from.id);
|
||||
const orderedFields = from.declaredOrder
|
||||
? from.declaredOrder.flatMap((n) => {
|
||||
const f = from.fields.find((x) => x.name === n);
|
||||
|
||||
@@ -298,7 +298,7 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (isStageCall(lowerer, expr)) return true;
|
||||
if (!ts.isIdentifier(expr)) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
|
||||
const decl = symbol ? symbol.valueDeclaration : undefined;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
@@ -1800,9 +1800,15 @@ function foldStages(
|
||||
...inner(value),
|
||||
];
|
||||
} else {
|
||||
// A take before this stage sets `done` as it delivers its last
|
||||
// value, which still flattens in full: the inner walk runs with the
|
||||
// flag clear, and keeps it set afterwards.
|
||||
const afterTake = slots.slice(0, si).some((slot) => slot.kind === "take");
|
||||
emit = (value) => {
|
||||
const innerT = s.fnT!.ret;
|
||||
const innerId = frame.local("fm", innerT, false);
|
||||
const heldId = afterTake ? frame.local("held", BOOL, false) : undefined;
|
||||
const walk = walkInner(lowerer, frame, innerId, innerT, done(), inner, loc);
|
||||
return [
|
||||
{
|
||||
kind: "varDecl",
|
||||
@@ -1811,7 +1817,26 @@ function foldStages(
|
||||
loc,
|
||||
},
|
||||
...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []),
|
||||
...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc),
|
||||
...(heldId === undefined
|
||||
? walk
|
||||
: [
|
||||
{ kind: "varDecl", localId: heldId, init: done(), loc } as IrStmt,
|
||||
{ kind: "assign", localId: doneId, value: boolLit(false, loc), loc } as IrStmt,
|
||||
...walk,
|
||||
{
|
||||
kind: "assign",
|
||||
localId: doneId,
|
||||
value: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: varRef(heldId, BOOL, loc),
|
||||
right: done(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
} as IrStmt,
|
||||
]),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { matchAllRowsReadByElements } from "./match-row-uses.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
boolLit,
|
||||
dynUndefinedExpr,
|
||||
nodeThrowExpr,
|
||||
numLit,
|
||||
strLit,
|
||||
varRef,
|
||||
} from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -316,6 +323,21 @@ export function lowerStringIndexCall(
|
||||
) {
|
||||
index = lowerer.coerceInto(indexNode, index, DYN);
|
||||
}
|
||||
return stringIndexHelperCall(lowerer, call, method, receiver, receiverNode, index, indexNode);
|
||||
}
|
||||
|
||||
/** The relative-index / code-point helper call over an already lowered
|
||||
* index (any type positionNumber converts — dyn indices included). */
|
||||
function stringIndexHelperCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
method: "at" | "codePointAt",
|
||||
receiver: IrExpr,
|
||||
receiverNode: ts.Node,
|
||||
index: IrExpr,
|
||||
indexNode: ts.Expression | undefined,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const valueType = method === "at" ? STRING : F64;
|
||||
const resultType = lowerer.withUndefinedArmOf(valueType);
|
||||
if (!resultType || resultType.kind !== "union")
|
||||
@@ -1061,7 +1083,8 @@ export function lowerStringMethodCall(
|
||||
const replaceMethod = STRING_REPLACE_METHODS.has(access.name.text)
|
||||
? (access.name.text as "replace" | "replaceAll")
|
||||
: null;
|
||||
if (!entry && !indexMethod && !replaceMethod) return null;
|
||||
const extraMethod = EXTRA_STRING_METHODS.has(access.name.text);
|
||||
if (!entry && !indexMethod && !replaceMethod && !extraMethod) return null;
|
||||
// A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
|
||||
// value) arrives pre-extracted through `dynReceiver`; its checker type
|
||||
// is `any`, so the type/symbol gates don't apply — the dyn value's
|
||||
@@ -1089,6 +1112,12 @@ export function lowerStringMethodCall(
|
||||
return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
}
|
||||
if (extraMethod && argumentNodes === call.arguments.nodes) {
|
||||
const receiver = dynReceiver
|
||||
? dynReceiver()
|
||||
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
|
||||
return lowerExtraStringMethodCall(lowerer, call, access.name.text, receiver, access.expression);
|
||||
}
|
||||
// The lib declares optional parameters beyond some lowered forms; fence
|
||||
// those arities instead of passing arguments the runtime doesn't take.
|
||||
if (
|
||||
@@ -1386,3 +1415,393 @@ function lowerLocaleCompareCall(
|
||||
lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!));
|
||||
return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/* ── String.prototype / Number.prototype members beyond the core tables ──
|
||||
* substr, the locale case pair, the Annex B HTML methods, toPrecision and
|
||||
* toExponential(fractionDigits) on typed receivers, plus the kind dispatch
|
||||
* for every String/Number method a checked-dynamic receiver can name
|
||||
* whose runtime dispatch had no answer (`character.codePointAt(0)` in a
|
||||
* JS package, `n.toPrecision(3)` on a JS-inferred number). Each path
|
||||
* replaces a compile-time refusal, a runtime 'not supported yet' fence or
|
||||
* Node's is-not-a-function answer for a real method, so programs that
|
||||
* never spell these names compile exactly as before. */
|
||||
|
||||
/** String.prototype members (Annex B included) whose typed-receiver
|
||||
* lowering lives here. */
|
||||
export const EXTRA_STRING_METHODS: ReadonlySet<string> = new Set([
|
||||
"substr",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
"anchor",
|
||||
"big",
|
||||
"blink",
|
||||
"bold",
|
||||
"fixed",
|
||||
"fontcolor",
|
||||
"fontsize",
|
||||
"italics",
|
||||
"link",
|
||||
"small",
|
||||
"strike",
|
||||
"sub",
|
||||
"sup",
|
||||
]);
|
||||
|
||||
/** String methods a dyn receiver dispatches by runtime kind here: a string
|
||||
* runs the typed lowering, every other kind takes the runtime dispatch
|
||||
* (an object's own member, Node's is-not-a-function otherwise). Only names
|
||||
* no other dyn kind's prototype declares: the shared ones (at, concat,
|
||||
* slice, ...) already dispatch at run time for arrays, and a kind test in
|
||||
* front of them would change array code that works today. The Annex B
|
||||
* HTML names (bold, italics, link, ...) stay out too: chalk-style builders
|
||||
* call them on functions and objects, whose calls must stay as they are. */
|
||||
const DYN_STRING_KIND_METHODS: ReadonlySet<string> = new Set([
|
||||
"substr",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
"codePointAt",
|
||||
"isWellFormed",
|
||||
"toWellFormed",
|
||||
"trimLeft",
|
||||
"trimRight",
|
||||
]);
|
||||
|
||||
/** Number methods a dyn receiver dispatches by runtime kind here. */
|
||||
const DYN_NUMBER_KIND_METHODS: ReadonlySet<string> = new Set(["toPrecision", "toExponential"]);
|
||||
|
||||
/** Annex B CreateHTML: the tag and the attribute (null for none). */
|
||||
const HTML_METHODS: Readonly<Record<string, readonly [string, string | null]>> = {
|
||||
anchor: ["a", "name"],
|
||||
big: ["big", null],
|
||||
blink: ["blink", null],
|
||||
bold: ["b", null],
|
||||
fixed: ["tt", null],
|
||||
fontcolor: ["font", "color"],
|
||||
fontsize: ["font", "size"],
|
||||
italics: ["i", null],
|
||||
link: ["a", "href"],
|
||||
small: ["small", null],
|
||||
strike: ["strike", null],
|
||||
sub: ["sub", null],
|
||||
sup: ["sup", null],
|
||||
};
|
||||
|
||||
export function isDynPrimitiveKindMethod(name: string): boolean {
|
||||
return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name);
|
||||
}
|
||||
|
||||
/** Evaluate `value` once into a hidden local (effect-free reads stay). */
|
||||
function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr {
|
||||
if (
|
||||
value.kind === "varRef" ||
|
||||
value.kind === "numLit" ||
|
||||
value.kind === "strLit" ||
|
||||
value.kind === "boolLit" ||
|
||||
value.kind === "unitLit"
|
||||
)
|
||||
return value;
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
|
||||
return varRef(local.id, value.type, value.loc);
|
||||
}
|
||||
|
||||
function seq(stmts: IrStmt[], result: IrExpr): IrExpr {
|
||||
return stmts.length === 0
|
||||
? result
|
||||
: { kind: "seqExpr", stmts, result, type: result.type, loc: result.loc };
|
||||
}
|
||||
|
||||
/** ToString of a method argument: the dyn coercion (symbols throw), the
|
||||
* typed conversions elsewhere. */
|
||||
function stringArg(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr {
|
||||
if (value.type.kind === "dyn")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [value],
|
||||
type: STRING,
|
||||
loc: value.loc,
|
||||
};
|
||||
return lowerer.ensureString(value, node);
|
||||
}
|
||||
|
||||
/** An optional numeric argument: its ToNumber value and whether it was
|
||||
* supplied (undefined counts as absent, as the spec's checks do). */
|
||||
function optionalNumber(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr | undefined,
|
||||
node: ts.Expression,
|
||||
stmts: IrStmt[],
|
||||
subject: string,
|
||||
loc: SrcLoc,
|
||||
): { value: IrExpr; has: IrExpr } {
|
||||
if (!value || value.type.kind === "undefinedT" || value.type.kind === "void")
|
||||
return { value: numLit(0, loc), has: boolLit(false, loc) };
|
||||
if (
|
||||
value.type.kind === "f64" ||
|
||||
value.type.kind === "string" ||
|
||||
value.type.kind === "bool" ||
|
||||
value.type.kind === "nullT"
|
||||
)
|
||||
return {
|
||||
value: positionNumber(lowerer, value, numLit(0, loc), node, subject),
|
||||
has: boolLit(true, loc),
|
||||
};
|
||||
const boxed =
|
||||
value.type.kind === "dyn"
|
||||
? value
|
||||
: hoist(lowerer, lowerer.coerceToExpected(value, DYN), stmts, "%numArg");
|
||||
const absent: IrExpr = { kind: "dynTest", test: "undefined", value: boxed, type: BOOL, loc };
|
||||
return {
|
||||
value: positionNumber(lowerer, boxed, numLit(0, loc), node, subject),
|
||||
has: { kind: "unary", op: "!", operand: absent, type: BOOL, loc },
|
||||
};
|
||||
}
|
||||
|
||||
/** The typed result of a String.prototype member over a STRING receiver
|
||||
* and already evaluated arguments, or null when the name is not one of
|
||||
* this module's. */
|
||||
function buildStringMethod(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
s: IrExpr,
|
||||
args: IrExpr[],
|
||||
receiverNode: ts.Node,
|
||||
stmts: IrStmt[],
|
||||
): IrExpr | null {
|
||||
const loc = locOf(call);
|
||||
const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call;
|
||||
if (name === "substr") {
|
||||
const start = args[0]
|
||||
? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start")
|
||||
: numLit(0, loc);
|
||||
const length = optionalNumber(lowerer, args[1], node(1), stmts, "substr length", loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "str.substr",
|
||||
args: [s, start, length.value, length.has],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "toLocaleLowerCase" || name === "toLocaleUpperCase") {
|
||||
const locales = args[0] ? lowerer.coerceToExpected(args[0], DYN) : dynUndefinedExpr(loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "str.localeCase",
|
||||
args: [s, locales, boolLit(name === "toLocaleUpperCase", loc)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const html = Object.hasOwn(HTML_METHODS, name) ? HTML_METHODS[name] : undefined;
|
||||
if (html) {
|
||||
const [tag, attribute] = html;
|
||||
const subject = hoist(lowerer, s, stmts, "%htmlSubject");
|
||||
const pieces: IrExpr[] = [strLit(`<${tag}`, loc)];
|
||||
if (attribute !== null) {
|
||||
const text = args[0] ? stringArg(lowerer, args[0], node(0)) : strLit("undefined", loc);
|
||||
pieces.push(strLit(` ${attribute}="`, loc));
|
||||
pieces.push({
|
||||
kind: "libCall",
|
||||
fn: "str.replaceLiteral",
|
||||
args: [text, strLit('"', loc), strLit(""", loc), boolLit(true, loc)],
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
pieces.push(strLit('"', loc));
|
||||
}
|
||||
pieces.push(strLit(">", loc), subject, strLit(`</${tag}>`, loc));
|
||||
return pieces.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc }));
|
||||
}
|
||||
if (name === "codePointAt")
|
||||
return stringIndexHelperCall(
|
||||
lowerer,
|
||||
call,
|
||||
name,
|
||||
s,
|
||||
receiverNode,
|
||||
args[0] ?? numLit(0, loc),
|
||||
call.arguments.nodes[0],
|
||||
);
|
||||
if (name === "isWellFormed" || name === "toWellFormed")
|
||||
return {
|
||||
kind: "strIntrinsic",
|
||||
method: name,
|
||||
receiver: s,
|
||||
args: [],
|
||||
type: name === "isWellFormed" ? BOOL : STRING,
|
||||
loc,
|
||||
};
|
||||
if (name === "trimLeft" || name === "trimRight")
|
||||
return {
|
||||
kind: "strIntrinsic",
|
||||
method: name === "trimLeft" ? "trimStart" : "trimEnd",
|
||||
receiver: s,
|
||||
args: [],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The typed result of toPrecision/toExponential over an F64 receiver. */
|
||||
function buildNumberMethod(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
x: IrExpr,
|
||||
args: IrExpr[],
|
||||
stmts: IrStmt[],
|
||||
): IrExpr | null {
|
||||
const loc = locOf(call);
|
||||
if (name !== "toPrecision" && name !== "toExponential") return null;
|
||||
const digits = optionalNumber(
|
||||
lowerer,
|
||||
args[0],
|
||||
call.arguments.nodes[0] ?? call,
|
||||
stmts,
|
||||
`${name} digits`,
|
||||
loc,
|
||||
);
|
||||
if (name === "toPrecision")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toPrecision",
|
||||
args: [x, digits.value, digits.has],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const shortest: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "num.toExponential",
|
||||
args: [x],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (digits.has.kind === "boolLit" && !digits.has.value) return shortest;
|
||||
const fixed: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "num.toExponentialDigits",
|
||||
args: [x, digits.value],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (digits.has.kind === "boolLit") return fixed;
|
||||
return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Evaluate the call's arguments, in order, into hidden locals. */
|
||||
function evaluatedArgs(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
stmts: IrStmt[],
|
||||
expected: IrType | undefined,
|
||||
): IrExpr[] {
|
||||
return call.arguments.nodes.map((arg) =>
|
||||
hoist(
|
||||
lowerer,
|
||||
expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg),
|
||||
stmts,
|
||||
"%methodArg",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver,
|
||||
* then arguments, then the method's own conversions — the spec order. */
|
||||
export function lowerExtraStringMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
receiverNode: ts.Node,
|
||||
): IrExpr | null {
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const s = hoist(lowerer, receiver, stmts, "%stringReceiver");
|
||||
const args = evaluatedArgs(lowerer, call, stmts, undefined);
|
||||
const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts);
|
||||
return result ? seq(stmts, result) : null;
|
||||
}
|
||||
|
||||
/** A typed F64 receiver calling toPrecision(p) or toExponential(f). */
|
||||
export function lowerExtraNumberMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
): IrExpr | null {
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length > 1) return null;
|
||||
if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1))
|
||||
return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const x = hoist(lowerer, receiver, stmts, "%numberReceiver");
|
||||
const args = evaluatedArgs(lowerer, call, stmts, undefined);
|
||||
const result = buildNumberMethod(lowerer, call, name, x, args, stmts);
|
||||
return result ? seq(stmts, result) : null;
|
||||
}
|
||||
|
||||
/** `recv.m(args)` on a checked-dynamic receiver for a String/Number method
|
||||
* the runtime dispatch cannot answer: the receiver and arguments evaluate
|
||||
* once, a string (or number) receiver runs the typed lowering, and every
|
||||
* other kind keeps the runtime dispatch's answer. Null for other names. */
|
||||
export function lowerDynPrimitiveMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
calleeName: string,
|
||||
recv: IrExpr,
|
||||
): IrExpr | null {
|
||||
const stringMethod = DYN_STRING_KIND_METHODS.has(name);
|
||||
const numberMethod = DYN_NUMBER_KIND_METHODS.has(name);
|
||||
if (!stringMethod && !numberMethod) return null;
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(call);
|
||||
const stmts: IrStmt[] = [];
|
||||
const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc });
|
||||
const r = varRef(local.id, DYN, loc);
|
||||
const args = evaluatedArgs(lowerer, call, stmts, DYN);
|
||||
let result: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: r,
|
||||
method: name,
|
||||
calleeName,
|
||||
args,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const branch = (
|
||||
test: "string" | "number",
|
||||
build: (value: IrExpr, branchStmts: IrStmt[]) => IrExpr | null,
|
||||
): void => {
|
||||
const branchStmts: IrStmt[] = [];
|
||||
const value: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: r,
|
||||
type: test === "string" ? STRING : F64,
|
||||
loc,
|
||||
};
|
||||
const built = build(value, branchStmts);
|
||||
if (!built) return;
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test, value: r, type: BOOL, loc },
|
||||
then: { kind: "dynFrom", value: seq(branchStmts, built), type: DYN, loc },
|
||||
else_: result,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
if (numberMethod)
|
||||
branch("number", (x, branchStmts) =>
|
||||
buildNumberMethod(lowerer, call, name, x, args, branchStmts),
|
||||
);
|
||||
if (stringMethod)
|
||||
branch("string", (s, branchStmts) =>
|
||||
buildStringMethod(lowerer, call, name, s, args, call.expression, branchStmts),
|
||||
);
|
||||
return seq(stmts, result);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface CycleInitFlag {
|
||||
* own module's early calls is checked opportunistically: whatever cannot
|
||||
* be checked keeps the unchecked behavior instead of refusing. */
|
||||
cycle: boolean;
|
||||
/** A pointer-guarded binding declared without an initializer: its
|
||||
* declaration initializes it (to undefined, which the pointer cannot
|
||||
* hold), so the flag alone decides its writes (markUninitializedWrites). */
|
||||
uninitialized?: true;
|
||||
/** A class binding: its references are the class's own IR forms and its
|
||||
* static field storage (`global` is the flag itself). */
|
||||
className?: string;
|
||||
@@ -107,6 +111,30 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void {
|
||||
registerInitFlag(lowerer, binding, global, false);
|
||||
}
|
||||
}
|
||||
// `let f: T;` with a pointer-guarded slot: once the declaration ran the
|
||||
// binding is initialized while the slot is still empty, so a write
|
||||
// checks the flag (where code can run before the declaration) instead of
|
||||
// the empty pointer. `f = () => f(...)` is the shape.
|
||||
for (const [id, entry] of lowerer.uninitializedLexicalGlobals) {
|
||||
const global = lowerer.globalsList.find((g) => g.id === id);
|
||||
if (global === undefined) continue;
|
||||
if (!lowerer.cycleInitFlags.has(id)) {
|
||||
registerInitFlag(
|
||||
lowerer,
|
||||
{
|
||||
symbol: entry.symbol,
|
||||
name: entry.name,
|
||||
kind: "lexical",
|
||||
module: entry.module,
|
||||
reference: entry.name,
|
||||
settled: new Set(),
|
||||
},
|
||||
global,
|
||||
false,
|
||||
);
|
||||
}
|
||||
lowerer.cycleInitFlags.get(id)!.uninitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** A class that its module's early-running code can reach before the class
|
||||
@@ -304,6 +332,22 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void
|
||||
const fileOfInit = new Map<string, ts.SourceFile>();
|
||||
for (const [sf, name] of lowerer.initNameOf) fileOfInit.set(name, sf);
|
||||
for (const fn of mod.functions) rewriteFunction(lowerer, fn, fileOfInit.get(fn.name));
|
||||
markUninitializedWrites(lowerer, mod);
|
||||
}
|
||||
|
||||
/** Writes to a binding declared without an initializer that run after its
|
||||
* declaration store into an initialized binding whose slot may still be
|
||||
* empty: the pointer guard must not refuse them. Writes that can run
|
||||
* earlier carry the flag check rewriteFunction inserted. */
|
||||
function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void {
|
||||
const ids = new Set<string>();
|
||||
for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id);
|
||||
if (ids.size === 0) return;
|
||||
const mark = (s: IrStmt): IrStmt =>
|
||||
s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s;
|
||||
const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt);
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt));
|
||||
for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt);
|
||||
}
|
||||
|
||||
function rewriteFunction(
|
||||
@@ -444,6 +488,7 @@ function rewriteFunction(
|
||||
kind: "assign",
|
||||
localId: s.localId,
|
||||
value: { kind: "varRef", localId: t, type: s.value.type, loc: s.loc },
|
||||
...(flag.uninitialized ? { initializes: true as const } : {}),
|
||||
loc: s.loc,
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -286,7 +286,7 @@ function registryInitializer(
|
||||
)
|
||||
return true;
|
||||
const symbol = lowerer.resolveValueSymbol(callee);
|
||||
const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
|
||||
const declaration = symbol && symbol.valueDeclaration;
|
||||
if (!symbol || !declaration || !bindingNeverReassigned(lowerer, symbol, declaration))
|
||||
return false;
|
||||
if (ts.isFunctionDeclaration(declaration)) return true;
|
||||
@@ -346,7 +346,7 @@ function registryInitializer(
|
||||
}
|
||||
if (packageSource && ts.isPropertyAccessExpression(node)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
|
||||
const declaration = symbol && symbol.valueDeclaration;
|
||||
const owner = ts.isIdentifier(node.expression)
|
||||
? lowerer.resolveValueSymbol(node.expression)
|
||||
: undefined;
|
||||
@@ -394,7 +394,7 @@ function registryInitializer(
|
||||
)
|
||||
return true;
|
||||
const symbol = lowerer.resolveValueSymbol(node);
|
||||
const functionDeclaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
|
||||
const functionDeclaration = symbol && symbol.valueDeclaration;
|
||||
if (
|
||||
functionDeclaration &&
|
||||
ts.isFunctionDeclaration(functionDeclaration) &&
|
||||
@@ -449,7 +449,7 @@ function registryOwnerIsData(
|
||||
symbol: ts.Symbol,
|
||||
statement: ts.Statement,
|
||||
): boolean {
|
||||
const declaration = lowerer.checker.valueDeclarationOf(symbol);
|
||||
const declaration = symbol.valueDeclaration;
|
||||
if (
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user