Repository: denoland/deno
Pull request: feat(ext/url): add `URL.parse`

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->

Closes #23069


Changed files:
filename: ext/url/00_url.js
patch: @@ -351,17 +351,29 @@ function trim(s) {
 // Represents a "no port" value. A port in URL cannot be greater than 2^16 - 1
 const NO_PORT = 65536;
 
+const skipInit = Symbol();
 const componentsBuf = new Uint32Array(8);
+
 class URL {
+  /** @type {URLSearchParams|null} */
   #queryObject = null;
+  /** @type {string} */
   #serialization;
+  /** @type {number} */
   #schemeEnd;
+  /** @type {number} */
   #usernameEnd;
+  /** @type {number} */
   #hostStart;
+  /** @type {number} */
   #hostEnd;
+  /** @type {number} */
   #port;
+  /** @type {number} */
   #pathStart;
+  /** @type {number} */
   #queryStart;
+  /** @type {number} */
   #fragmentStart;
 
   [_updateUrlSearch](value) {
@@ -378,18 +390,46 @@ class URL {
    * @param {string} [base]
    */
   constructor(url, base = undefined) {
+    // skip initialization for URL.parse
+    if (url === skipInit) {
+      return;
+    }
     const prefix = "Failed to construct 'URL'";
     webidl.requiredArguments(arguments.length, 1, prefix);
     url = webidl.converters.DOMString(url, prefix, "Argument 1");
     if (base !== undefined) {
       base = webidl.converters.DOMString(base, prefix, "Argument 2");
     }
-    this[webidl.brand] = webidl.brand;
     const status = opUrlParse(url, base);
+    this[webidl.brand] = webidl.brand;
     this.#serialization = getSerialization(status, url, base);
     this.#updateComponents();
   }
 
+  /**
+   * @param {string} url
+   * @param {string} [base]
+   */
+  static parse(url, base = undefined) {
+    const prefix = "Failed to execute 'URL.parse'";
+    webidl.requiredArguments(arguments.length, 1, prefix);
+    url = webidl.converters.DOMString(url, prefix, "Argument 1");
+    if (base !== undefined) {
+      base = webidl.converters.DOMString(base, prefix, "Argument 2");
+    }
+    const status = opUrlParse(url, base);
+    if (status !== 0 && status !== 1) {
+      return null;
+    }
+    // If initialized with webidl.createBranded, private properties are not be accessible,
+    // so it is passed through the constructor
+    const self = new this(skipInit);
+    self[webidl.brand] = webidl.brand;
+    self.#serialization = getSerialization(status, url, base);
+    self.#updateComponents();
+    return self;
+  }
+
   /**
    * @param {string} url
    * @param {string} [base]
@@ -799,7 +839,7 @@ class URL {
     }
   }
 
-  /** @return {string} */
+  /** @return {URLSearchParams} */
   get searchParams() {
     if (this.#queryObject == null) {
       this.#queryObject = new URLSearchParams(this.search);

filename: ext/url/lib.deno_url.d.ts
patch: @@ -195,6 +195,7 @@ declare interface URL {
 declare var URL: {
   readonly prototype: URL;
   new (url: string | URL, base?: string | URL): URL;
+  parse(url: string | URL, base?: string | URL): URL | null;
   canParse(url: string | URL, base?: string | URL): boolean;
   createObjectURL(blob: Blob): string;
   revokeObjectURL(url: string): void;