Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- [`node:test`](./interop/nodejs-builtins/supported-modules/test.md)
- [`node:timers`](./interop/nodejs-builtins/supported-modules/timers.md)
- [`node:tty`](./interop/nodejs-builtins/supported-modules/tty.md)
- [`node:url`](./interop/nodejs-builtins/supported-modules/url.md)
- [Troubleshooting]()
- [Common issues](./troubleshooting/common-issues.md)
- [Contributor Guide]()
Expand Down
1 change: 0 additions & 1 deletion docs/src/interop/nodejs-builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ the module or upstream project.
| Modules | Why they are not enabled yet |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node:trace_events` | The fallbacks preserve useful shapes, but tracing is synthetic or no-op without runtime integration. |
| `node:url` | There is substantial Node-derived code, but its eager `node:path` dependency adds a WASI environment requirement even for global-only URL use, and its namespace combines modern and legacy APIs that need separate policy. |

### Host-backed or broad subsystems

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ compatibility limits. Related submodules share their parent API page. See the
| [`node:test`](./test.md), [`node:test/reporters`](./test.md) | Serial component tests, hooks, assertions, mocks, and reporters. No additional WIT imports. Runner requires engine `AbortController`; see the API page for engine limits. |
| [`node:timers`](./timers.md), [`node:timers/promises`](./timers.md) | Node 24 timer handles and promise timers over engine task scheduling; see the API page for runtime limits. |
| [`node:tty`](./tty.md) | Node 24.20 `isatty`, `ReadStream` and `WriteStream` over the host process's descriptors through an explicit host capability; denied by default. |
| [`node:url`](./url.md) | Node 24 URL, URLSearchParams, URLPattern, domain and file conversions; relative file paths use optional WASI environment imports. |

[Globals](./globals.md) and [Errors](./errors.md) document runtime-wide Node.js
APIs. They are not importable as `node:globals` or `node:errors`.
72 changes: 72 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# `node:url`

| Imports | Implementation |
| --- | --- |
| `node:url` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/url` |

`node:url` supports URL construction and mutation, live `URLSearchParams`,
`URLPattern`, internationalized domains, file URL conversions, formatting, and
HTTP request options. The module and global `URL`/`URLSearchParams` constructors
share identity. Ordinary application imports work with `jco componentize --bundle`
on QuickJS and StarlingMonkey:

```js
import { URL, URLPattern, pathToFileURL, fileURLToPathBuffer } from 'node:url';

const endpoint = new URL('../items', 'https://example.com/api/');
endpoint.searchParams.append('tag', 'two words');
const route = new URLPattern({ pathname: '/items/:id' });
const file = pathToFileURL('/data/a b.txt');
const bytes = fileURLToPathBuffer('file:///data/%FF');
```

## File paths and capabilities

URL parsing, domain conversion, formatting, HTTP options and absolute file paths
need no WIT imports. Relative `pathToFileURL()` paths use the selected world's
`wasi:cli/environment@0.2.x` interface lazily. Missing or ambiguous environment
imports produce an explicit error when cwd resolution is needed; importing the
module and using its pure operations still works.

The default path convention is POSIX. `{ windows: true }` enables drive and UNC
paths on either backend. `fileURLToPathBuffer()` returns the same Buffer type as
`node:buffer`, preserving raw bytes and malformed percent escapes. Unlike the
string conversion, Node 24's Buffer conversion permits encoded slash bytes.

## Compatibility target and implementation

The target is Node **v24.20.0**, commit
`71b8b174857e25106d39b61a9e6f30d927da8b01`. The portable helpers are adapted from
Node's MIT-licensed `lib/url.js` and `lib/internal/url.js`. The WHATWG core is
`whatwg-url@14.2.0`, with `tr46@5.1.1`, `webidl-conversions@7.0.0`, and
`punycode@2.3.1`; pattern matching uses `urlpattern-polyfill@10.1.0`.

Jco adds Node's constructor coercion, error codes, legacy object formatting, and
lazy path providers. Its UTF-8 adapter handles malformed sequences consistently
across engines; the decoder is adapted from Apache-2.0-licensed
`text-decoder@1.2.7`. StarlingMonkey's native URL host parser supplies IDNA
normalization because that engine lacks `String.normalize()`. At bundle time,
`regexpu-core@6.4.0` expands URLPattern's two Unicode identifier expressions for
StarlingMonkey. Only the exact audited dependency files receive these adapters.

The installed `unenv@2.0.0-rc.24` URL implementation was not admitted: it lacks
`URLPattern` and `fileURLToPathBuffer`, uses Punycode without domain validation,
and differs in Windows paths, Unicode formatting, and absent HTTP option fields.
The new implementation continues to share Jco's audited Buffer and querystring
cores. Applications can mix ordinary `node:` imports with direct jco-std adapters;
the latter expose explicit factories for callers supplying their own providers.

## Intentional differences

Deprecated string parsing is refused immediately with
`ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API`: `parse()`, `resolve()`,
`resolveObject()`, `format(string)`, and the corresponding legacy parsing methods.
These errors occur before argument coercion or callbacks. Use `new URL(input, base)`
or `URL.parse(input, base)`. Legacy `Url` construction, object formatting,
`parseHost()`, and `Url.prototype.resolveObject(object)` remain functional.

`URL.createObjectURL()` and `URL.revokeObjectURL()` throw
`ERR_JCO_UNSUPPORTED_NODE_API`; Jco does not provide Node's thread-local Blob URL
registry. Invalid Punycode labels can be rejected more strictly by the WHATWG
fallback than by Node's Ada parser. Engine-specific inspection and stack formatting
are not reproduced. Errors mentioning the file host platform use `posix`.
3 changes: 3 additions & 0 deletions packages/jco-std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ Jco can bundle the following Node.js APIs into JavaScript WebAssembly components
- the `node:net` TCP client/server and address APIs over `wasi:sockets`;
- `node:buffer`, with its modern core provided by Jco's audited unenv
compatibility layer;
- [`node:url`](../../docs/src/interop/nodejs-builtins/supported-modules/url.md),
with portable WHATWG URL/URLSearchParams, URLPattern, domain and
file conversions, and lazy WASI cwd access for relative paths;
- `node:querystring`, provided by Jco's audited unenv compatibility layer;
- `node:events`, whose `EventEmitter` comes from Jco's audited unenv
compatibility layer, completed by
Expand Down
20 changes: 19 additions & 1 deletion packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,21 @@
"types": "./dist/wasi/0.2.x/node/24.x.x/test/reporters.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/test/reporters.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/test/reporters.js"
},
"./wasi/0.2.x/node/24.x.x/url": {
"types": "./dist/wasi/0.2.x/node/24.x.x/url.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/url.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/url.js"
},
"./wasi/0.2.x/node/24.x.x/url/encoding": {
"types": "./dist/wasi/0.2.x/node/24.x.x/url/encoding.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/url/encoding.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/url/encoding.js"
},
"./wasi/0.2.x/node/24.x.x/url/idna": {
"types": "./dist/wasi/0.2.x/node/24.x.x/url/idna.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/url/idna.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/url/idna.js"
}
},
"scripts": {
Expand All @@ -500,7 +515,10 @@
"acorn": "8.17.0",
"acorn-walk": "8.3.5",
"minimatch": "10.2.6",
"readable-stream": "4.7.0"
"punycode": "2.3.1",
"readable-stream": "4.7.0",
"urlpattern-polyfill": "10.1.0",
"whatwg-url": "14.2.0"
},
"devDependencies": {
"@bytecodealliance/componentize-js": "^0.22.0",
Expand Down
52 changes: 52 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** Node 24 URL module assembled over portable WHATWG cores and lazy WASI paths. */
import { createPath, type PathProviders } from "./path.js";
import { URL, URLPattern, URLSearchParams as CoreSearchParams } from "./url/whatwg.js";
import { adaptSearchParams } from "./url/search-params.js";
import { domainToASCII, domainToUnicode } from "./url/domain.js";
import { createPathToFileURL, fileURLToPath, fileURLToPathBuffer } from "./url/file.js";
import { format } from "./url/format.js";
import { Url, parse, resolve, resolveObject } from "./url/legacy.js";
import { urlToHttpOptions } from "./url/http-options.js";

export type * from "./url/types.js";
export interface UrlModule {
Url: typeof Url;
parse: typeof parse;
resolve: typeof resolve;
resolveObject: typeof resolveObject;
format: typeof format;
URL: typeof URL;
URLPattern: typeof URLPattern;
URLSearchParams: typeof globalThis.URLSearchParams;
domainToASCII: typeof domainToASCII;
domainToUnicode: typeof domainToUnicode;
pathToFileURL: ReturnType<typeof createPathToFileURL>;
fileURLToPath: typeof fileURLToPath;
fileURLToPathBuffer: typeof fileURLToPathBuffer;
urlToHttpOptions: typeof urlToHttpOptions;
}
const URLSearchParams = adaptSearchParams(CoreSearchParams);
Object.defineProperty(URLSearchParams.prototype, "constructor", {
value: URLSearchParams,
writable: true,
configurable: true,
});

export function createUrl(providers: PathProviders): UrlModule {
return {
Url,
parse,
resolve,
resolveObject,
format,
URL,
URLPattern,
URLSearchParams,
domainToASCII,
domainToUnicode,
pathToFileURL: createPathToFileURL(createPath(providers)),
fileURLToPath,
fileURLToPathBuffer,
urlToHttpOptions,
};
}
58 changes: 58 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/url/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

/**
* Node v24.20.0 lib/internal/url.js domain conversion contract, commit
* 71b8b174857e25106d39b61a9e6f30d927da8b01 (MIT, see LICENSE).
* Ada's host parser is replaced with WHATWG URL host parsing (UTS46, IPv4,
* IPv6 and forbidden characters); punycode@2.3.1 only decodes validated hosts.
*/
import punycode from "punycode/punycode.js";
import { missingArgs } from "../errors.js";
import { URL } from "./whatwg.js";

export function domainToASCII(domain: string): string {
if (arguments.length === 0) {
throw missingArgs("domain");
}
const text = `${domain}`;
if (!text) {
return "";
}
const url = new URL("http://jco-invalid.invalid");
url.hostname = text;
if (url.hostname === "jco-invalid.invalid") {
// A hostname setter leaves its previous value intact on parse failure.
// A second sentinel distinguishes a valid input equal to the first one.
url.hostname = "jco-second.invalid";
url.hostname = text;
if (url.hostname === "jco-second.invalid") {
return "";
}
}
return url.hostname;
}
export function domainToUnicode(domain: string): string {
if (arguments.length === 0) {
throw missingArgs("domain");
}
return punycode.toUnicode(domainToASCII(domain));
}
75 changes: 75 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/url/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Adapter for whatwg-url@14.2.0/lib/encoding.js (jsdom, MIT).
* Jco substitutes this precise internal module when bundling so QuickJS need
* not provide TextEncoder/TextDecoder. Encoding uses the shared Buffer core;
* decoding follows the UTF-8 state machine, including maximal-subpart errors.
*/
import { Buffer } from "node:buffer";

export function utf8Encode(value: string): Uint8Array {
return Buffer.from(value, "utf8");
}
/**
* Adapted from holepunchto/text-decoder@1.2.7 lib/utf8-decoder.js, Apache-2.0.
* Copyright Holepunch. See the Apache-2.0 terms in this package's LICENSE.
* The streaming state machine is made local to a single complete input. Its
* b4a fast path is removed because Feross Buffer differs on malformed UTF-8.
*/
export function utf8DecodeWithoutBOM(bytes: Uint8Array): string {
let result = "";
let codePoint = 0;
let bytesNeeded = 0;
let bytesSeen = 0;
let lowerBoundary = 0x80;
let upperBoundary = 0xbf;
for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
if (bytesNeeded === 0) {
if (byte <= 0x7f) {
result += String.fromCharCode(byte);
} else if (byte >= 0xc2 && byte <= 0xdf) {
bytesNeeded = 2;
bytesSeen = 1;
codePoint = byte & 0x1f;
} else if (byte >= 0xe0 && byte <= 0xef) {
if (byte === 0xe0) {
lowerBoundary = 0xa0;
} else if (byte === 0xed) {
upperBoundary = 0x9f;
}
bytesNeeded = 3;
bytesSeen = 1;
codePoint = byte & 0xf;
} else if (byte >= 0xf0 && byte <= 0xf4) {
if (byte === 0xf0) {
lowerBoundary = 0x90;
} else if (byte === 0xf4) {
upperBoundary = 0x8f;
}
bytesNeeded = 4;
bytesSeen = 1;
codePoint = byte & 0x7;
} else {
result += "\ufffd";
}
continue;
}
if (byte < lowerBoundary || byte > upperBoundary) {
result += "\ufffd";
i--;
codePoint = bytesNeeded = bytesSeen = 0;
lowerBoundary = 0x80;
upperBoundary = 0xbf;
continue;
}
lowerBoundary = 0x80;
upperBoundary = 0xbf;
codePoint = (codePoint << 6) | (byte & 0x3f);
bytesSeen++;
if (bytesSeen === bytesNeeded) {
result += String.fromCodePoint(codePoint);
codePoint = bytesNeeded = bytesSeen = 0;
}
}
return bytesNeeded > 0 ? result + "\ufffd" : result;
}
23 changes: 23 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/url/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { codedError } from "../errors.js";

export function deprecated(api: string): never {
throw codedError(
new Error(`The deprecated ${api} API is not supported; use the WHATWG URL API instead`),
"ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API",
);
}
export function unsupported(api: string): never {
throw codedError(
new Error(`${api} is not supported by the Jco component runtime`),
"ERR_JCO_UNSUPPORTED_NODE_API",
);
}
export function invalidURL(
input: string,
base?: string,
): TypeError & { code: string; input: string; base?: string } {
const error = Object.assign(codedError(new TypeError("Invalid URL"), "ERR_INVALID_URL"), {
input,
});
return base === undefined ? error : Object.assign(error, { base });
}
Loading
Loading