Skip to content

feat: support extraURLs and detect portless by env by default#228

Merged
pi0 merged 10 commits into
unjs:mainfrom
onmax:feat/portless-url
Apr 30, 2026
Merged

feat: support extraURLs and detect portless by env by default#228
pi0 merged 10 commits into
unjs:mainfrom
onmax:feat/portless-url

Conversation

@onmax

@onmax onmax commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Adds a generic extraURLs option for showing integration-managed URLs in listhen output, including env-backed values for cases where the URL is only known at runtime.

resolves #227.

Summary by CodeRabbit

  • New Features

    • Display multiple titled "extra" URLs in CLI output, each with its own label, color, and aligned spacing.
  • Behavior Changes

    • Extra URLs may be provided as static values or as env-key references resolved when URLs are shown; static values override env lookups.
    • Duplicate URLs are deduplicated in the displayed list.
  • Documentation

    • Added docs for the extra-URLs option.
  • Tests

    • Added tests for env-backed resolution, overrides, lazy resolution, and deduplication.

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds declarative extraURLs support: new ExtraURL type and ListenURL.type = "extra", lazy resolution of env-backed extra URLs in getURLs(), storing per-URL title, and updated formatting/display logic; tests and README updated accordingly.

Changes

Cohort / File(s) Summary
Types / API
src/types.ts
Add exported ExtraURL (title: string, url?: string, env?: string); add extraURLs?: ExtraURL[] to ListenOptions; extend ListenURL with optional title and include "extra" in its type.
URL resolution & formatting
src/listen.ts
Extend _addURL to accept/store title; getURLs() merges caller extraURLs (or default Portless/env entry), resolves each via extraURL.url or process.env[extraURL.env] at call time, tags type: "extra", derives label/color using `title
Tests
test/index.test.ts
Add tests for lazy resolution of env-backed extraURLs, explicit extraURLs[].url overriding env, de-duplication when an extra URL matches an existing URL, and clearing process.env.PORTLESS_URL between tests.
Docs
README.md
Document extraURLs option shape (title + url or env), default Portless entry from PORTLESS_URL, and that env-backed values are resolved when URLs are shown.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I found a title, sniffed an env,
Kept URLs tidy, one and then,
Resolved at show, not when I start,
Blue labels hopping, neat and smart.
Hop on—extra links warm my heart ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature additions: supporting extraURLs for displaying additional URLs and detecting Portless URLs via environment variables by default.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 49.64%. Comparing base (d115045) to head (13ca678).
⚠️ Report is 53 commits behind head on main.

Files with missing lines Patch % Lines
src/listen.ts 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #228      +/-   ##
==========================================
+ Coverage   49.42%   49.64%   +0.22%     
==========================================
  Files          17        9       -8     
  Lines        1819      568    -1251     
  Branches      147      198      +51     
==========================================
- Hits          899      282     -617     
+ Misses        915      231     -684     
- Partials        5       55      +50     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pi0

pi0 commented Apr 30, 2026

Copy link
Copy Markdown
Member

I think it should be displayed additionally vs overring public url. #227 (comment)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/index.test.ts (1)

15-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore the original PORTLESS_URL value instead of always deleting it.

Line 20 unconditionally deletes the env var; if it was set before this suite, that state is lost for later tests. Snapshot and restore the original value for safer test isolation.

Suggested patch
 describe("listhen", () => {
   let listener: Listener | undefined;
+  const originalPortlessURL = process.env.PORTLESS_URL;
 
   afterEach(async () => {
     if (listener) {
       await listener.close();
       listener = undefined;
     }
-    delete process.env.PORTLESS_URL;
+    if (originalPortlessURL === undefined) {
+      delete process.env.PORTLESS_URL;
+    } else {
+      process.env.PORTLESS_URL = originalPortlessURL;
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 15 - 21, The afterEach currently deletes
process.env.PORTLESS_URL unconditionally and should instead restore the pre-test
value: add a top-level variable (e.g., originalPortlessUrl) and capture
process.env.PORTLESS_URL in a beforeAll or beforeEach hook, then in the
afterEach for the listener test restore that value by setting
process.env.PORTLESS_URL = originalPortlessUrl when defined or deleting it when
undefined; update the afterEach block that references listener to use this
restore logic and clear originalPortlessUrl if desired.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@test/index.test.ts`:
- Around line 15-21: The afterEach currently deletes process.env.PORTLESS_URL
unconditionally and should instead restore the pre-test value: add a top-level
variable (e.g., originalPortlessUrl) and capture process.env.PORTLESS_URL in a
beforeAll or beforeEach hook, then in the afterEach for the listener test
restore that value by setting process.env.PORTLESS_URL = originalPortlessUrl
when defined or deleting it when undefined; update the afterEach block that
references listener to use this restore logic and clear originalPortlessUrl if
desired.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61ea975a-ed92-4a01-92e7-7f5ddc5979c2

📥 Commits

Reviewing files that changed from the base of the PR and between 60ba9f2 and 18c787d.

📒 Files selected for processing (2)
  • src/listen.ts
  • test/index.test.ts

@onmax onmax changed the title feat: show PORTLESS_URL when present feat: support additional URLs in CLI output Apr 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/index.test.ts (1)

171-221: ⚡ Quick win

Add a dedicated test for publicURL vs PORTLESS_URL precedence

These tests cover additionalURLs precedence well, but they don’t directly assert the PR’s stated contract that explicit publicURL overrides PORTLESS_URL. A focused test here would lock that behavior and prevent regressions.

Suggested test addition
+  test("prefers explicit publicURL over PORTLESS_URL fallback", async () => {
+    process.env.PORTLESS_URL = "https://fallback.portless.dev";
+    listener = await listen(handle, {
+      hostname: "localhost",
+      publicURL: "https://configured.example.com",
+      additionalURLs: [{ title: "Portless", env: "PORTLESS_URL" }],
+    });
+
+    const urls = await listener.getURLs();
+    expect(urls).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          url: "https://configured.example.com",
+        }),
+      ]),
+    );
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 171 - 221, Add a focused unit test that
ensures an explicit publicURL passed to listen takes precedence over the
PORTLESS_URL env var: set process.env.PORTLESS_URL to a fallback value, call
listen(handle, { hostname: "localhost", publicURL:
"https://explicit.example.com" }), then assert listener.getURLs() contains an
object with type "public" and url "https://explicit.example.com" and does not
contain the fallback PORTLESS_URL; use the same pattern as existing
additionalURLs tests and reference listen, publicURL, process.env.PORTLESS_URL,
handle, and listener.getURLs to locate where to add the test.
src/types.ts (1)

188-203: ⚡ Quick win

Require at least one source field in AdditionalURL

AdditionalURL currently allows entries with only title, which makes invalid config type-check. Tightening this prevents no-op/undefined URL entries from reaching runtime.

Proposed type tightening
-export interface AdditionalURL {
-  /**
-   * The label shown in the CLI output.
-   */
-  title: string;
-
-  /**
-   * Static URL to show.
-   */
-  url?: string;
-
-  /**
-   * Environment variable that contains the URL to show.
-   */
-  env?: string;
-}
+export type AdditionalURL =
+  | {
+      /**
+       * The label shown in the CLI output.
+       */
+      title: string;
+      /**
+       * Static URL to show.
+       */
+      url: string;
+      /**
+       * Optional fallback env when url is absent in downstream overrides.
+       */
+      env?: string;
+    }
+  | {
+      /**
+       * The label shown in the CLI output.
+       */
+      title: string;
+      /**
+       * Environment variable that contains the URL to show.
+       */
+      env: string;
+      /**
+       * Optional explicit URL; when provided, runtime can still prefer url.
+       */
+      url?: string;
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 188 - 203, AdditionalURL currently allows only
title and can be empty at runtime; update the type to require at least one
source by replacing the interface with a union that combines title with either a
required url or a required env (e.g., make AdditionalURL = { title: string } &
({ url: string; env?: string } | { env: string; url?: string })), and adjust any
usages of AdditionalURL to satisfy the stronger type (functions or places
creating AdditionalURL objects must provide url or env).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/types.ts`:
- Around line 188-203: AdditionalURL currently allows only title and can be
empty at runtime; update the type to require at least one source by replacing
the interface with a union that combines title with either a required url or a
required env (e.g., make AdditionalURL = { title: string } & ({ url: string;
env?: string } | { env: string; url?: string })), and adjust any usages of
AdditionalURL to satisfy the stronger type (functions or places creating
AdditionalURL objects must provide url or env).

In `@test/index.test.ts`:
- Around line 171-221: Add a focused unit test that ensures an explicit
publicURL passed to listen takes precedence over the PORTLESS_URL env var: set
process.env.PORTLESS_URL to a fallback value, call listen(handle, { hostname:
"localhost", publicURL: "https://explicit.example.com" }), then assert
listener.getURLs() contains an object with type "public" and url
"https://explicit.example.com" and does not contain the fallback PORTLESS_URL;
use the same pattern as existing additionalURLs tests and reference listen,
publicURL, process.env.PORTLESS_URL, handle, and listener.getURLs to locate
where to add the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5ecc14d-e9fd-4e43-85b7-fe68d18df3ee

📥 Commits

Reviewing files that changed from the base of the PR and between 18c787d and 4821537.

📒 Files selected for processing (4)
  • README.md
  • src/listen.ts
  • src/types.ts
  • test/index.test.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/listen.ts

@pi0 pi0 changed the title feat: support additional URLs in CLI output feat: support additionalURLs Apr 30, 2026
@onmax

onmax commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Done!

should i create another PR to move tunnel to additional?

Co-authored-by: Copilot <copilot@github.com>
@pi0 pi0 changed the title feat: support additionalURLs feat: support additionalURLs and detect portless by env Apr 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/listen.ts`:
- Around line 226-241: The current additionalURLs loop in listen.ts (using
listhenOptions.additionalURLs, additionalURLs, and pushing into urls) can add
duplicates that other URL-type code avoids; modify the loop to apply the same
de-dup logic used elsewhere before pushing: compute the resolved url (from
additionalURL.url or process.env[additionalURL.env]) and only push an object
into urls if that exact url string is not already present in urls (or call the
existing dedupe helper used for other types if one exists), preserving
type/title/url fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1013c456-6758-40b4-992a-40da66afa6ad

📥 Commits

Reviewing files that changed from the base of the PR and between 4821537 and 4076e93.

📒 Files selected for processing (1)
  • src/listen.ts

Comment thread src/listen.ts Outdated
@pi0 pi0 changed the title feat: support additionalURLs and detect portless by env feat: support extraURLs and detect portless by env Apr 30, 2026
@pi0 pi0 changed the title feat: support extraURLs and detect portless by env feat: support extraURLs and detect portless by env by default Apr 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/index.test.ts (1)

15-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve pre-existing PORTLESS_URL instead of always deleting it.

Line 20 always deletes the variable, which can unintentionally alter global test-process state outside this suite.

Suggested fix
 describe("listhen", () => {
   let listener: Listener | undefined;
+  const originalPortlessURL = process.env.PORTLESS_URL;

   afterEach(async () => {
     if (listener) {
       await listener.close();
       listener = undefined;
     }
-    delete process.env.PORTLESS_URL;
+    if (originalPortlessURL === undefined) {
+      delete process.env.PORTLESS_URL;
+    } else {
+      process.env.PORTLESS_URL = originalPortlessURL;
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 15 - 21, afterEach currently always deletes
process.env.PORTLESS_URL which can leak state; change the teardown to restore
the original value captured before tests instead: add a top-level variable
(e.g., originalPortlessUrl) and capture its value in beforeEach (or at module
init) and then in afterEach, if originalPortlessUrl is undefined delete
PORTLESS_URL, otherwise set process.env.PORTLESS_URL = originalPortlessUrl; keep
the existing listener.close() logic intact (refer to afterEach and listener).
🧹 Nitpick comments (1)
test/index.test.ts (1)

171-235: ⚡ Quick win

Add coverage for the implicit default Portless path (extraURLs omitted).

The suite validates explicit extraURLs, but not the new default fallback behavior wired in src/listen.ts (defaulting to PORTLESS_URL when extraURLs is absent).

Suggested test
   describe("extra urls", () => {
+    test("includes default Portless url when extraURLs is omitted", async () => {
+      process.env.PORTLESS_URL = "https://default.portless.dev";
+      listener = await listen(handle, {
+        hostname: "localhost",
+      });
+
+      expect(await listener.getURLs()).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            type: "extra",
+            title: "Portless",
+            url: "https://default.portless.dev",
+          }),
+        ]),
+      );
+    });
+
     test("includes env-backed urls (resolved at getURLs time)", async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 171 - 235, Add a test to cover the implicit
default Portless extra URL when extraURLs is omitted: call listen(handle, {
hostname: "localhost" }) (no extraURLs), set process.env.PORTLESS_URL (either
before or after listen to assert lazy resolution), then call listener.getURLs()
and assert the returned array contains an object with type: "extra", title:
"Portless", and url equal to process.env.PORTLESS_URL; ensure you use the
existing listener variable and clean up/reset process.env.PORTLESS_URL after the
test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@test/index.test.ts`:
- Around line 15-21: afterEach currently always deletes process.env.PORTLESS_URL
which can leak state; change the teardown to restore the original value captured
before tests instead: add a top-level variable (e.g., originalPortlessUrl) and
capture its value in beforeEach (or at module init) and then in afterEach, if
originalPortlessUrl is undefined delete PORTLESS_URL, otherwise set
process.env.PORTLESS_URL = originalPortlessUrl; keep the existing
listener.close() logic intact (refer to afterEach and listener).

---

Nitpick comments:
In `@test/index.test.ts`:
- Around line 171-235: Add a test to cover the implicit default Portless extra
URL when extraURLs is omitted: call listen(handle, { hostname: "localhost" })
(no extraURLs), set process.env.PORTLESS_URL (either before or after listen to
assert lazy resolution), then call listener.getURLs() and assert the returned
array contains an object with type: "extra", title: "Portless", and url equal to
process.env.PORTLESS_URL; ensure you use the existing listener variable and
clean up/reset process.env.PORTLESS_URL after the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e7392d-2e87-4d2a-bdf4-76fdd3b9f1f9

📥 Commits

Reviewing files that changed from the base of the PR and between 4076e93 and bed9013.

📒 Files selected for processing (4)
  • README.md
  • src/listen.ts
  • src/types.ts
  • test/index.test.ts
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • src/types.ts

@pi0

pi0 commented Apr 30, 2026

Copy link
Copy Markdown
Member

Added portless env to defaults

image

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/listen.ts`:
- Around line 224-225: The current assignment for extraURLs uses logical OR
which treats an explicit empty array as falsy and overrides it with the default;
change the fallback chain to use nullish coalescing so only null/undefined fall
back — update the expression that sets extraURLs (referencing
getURLOptions.extraURLs, listhenOptions.extraURLs, and the default Portless
entry) to use ?? between operands instead of ||.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d102288e-b5ea-48df-8ec5-eb5bd877c893

📥 Commits

Reviewing files that changed from the base of the PR and between bed9013 and 22a3ae1.

📒 Files selected for processing (2)
  • src/listen.ts
  • src/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/types.ts

Comment thread src/listen.ts Outdated
@pi0 pi0 merged commit f341eb9 into unjs:main Apr 30, 2026
3 checks passed
@onmax onmax deleted the feat/portless-url branch April 30, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Portless Integration

2 participants