which-url 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/index.cjs +224 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +184 -0
- package/dist/normalize.d.ts +2 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/resolve.d.ts +3 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# which-url
|
|
2
|
+
|
|
3
|
+
Auto-detect your app's URL across hosting providers. Zero config.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install which-url
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import whichUrl from 'which-url'
|
|
11
|
+
|
|
12
|
+
whichUrl.href // "https://myapp.vercel.app"
|
|
13
|
+
whichUrl.hostname // "myapp.vercel.app"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Works on Vercel, Netlify, Cloudflare Pages, Railway, Fly.io, Render, DigitalOcean, and Heroku — automatically.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Named exports (plain strings — zero type friction)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { href, hostname, origin, isProduction } from 'which-url'
|
|
24
|
+
|
|
25
|
+
// Pass directly to any function that expects a string
|
|
26
|
+
process.env.BETTER_AUTH_URL = href
|
|
27
|
+
fetch(`${href}/api/data`)
|
|
28
|
+
cookie.domain = hostname
|
|
29
|
+
|
|
30
|
+
if (isProduction) {
|
|
31
|
+
// production-only logic
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Default export (object with dot access)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import whichUrl from 'which-url'
|
|
39
|
+
|
|
40
|
+
whichUrl.href // "https://myapp.vercel.app"
|
|
41
|
+
whichUrl.origin // "https://myapp.vercel.app"
|
|
42
|
+
whichUrl.hostname // "myapp.vercel.app"
|
|
43
|
+
whichUrl.host // "myapp.vercel.app"
|
|
44
|
+
whichUrl.protocol // "https:"
|
|
45
|
+
whichUrl.port // ""
|
|
46
|
+
|
|
47
|
+
whichUrl.env // "production" | "preview" | "local"
|
|
48
|
+
whichUrl.isProduction // boolean
|
|
49
|
+
whichUrl.isPreview // boolean
|
|
50
|
+
whichUrl.isLocal // boolean
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Property names follow the [WHATWG URL spec](https://url.spec.whatwg.org/) — nothing new to learn.
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
`which-url` reads environment variables that hosting providers set automatically. No configuration needed.
|
|
58
|
+
|
|
59
|
+
**Resolution priority:**
|
|
60
|
+
|
|
61
|
+
1. `APP_URL` env var (your override — always wins)
|
|
62
|
+
2. Provider auto-detection
|
|
63
|
+
3. `window.location.origin` (browser)
|
|
64
|
+
4. `http://localhost:${PORT || 3000}` (development)
|
|
65
|
+
5. Throws in production if nothing detected
|
|
66
|
+
|
|
67
|
+
## Provider support
|
|
68
|
+
|
|
69
|
+
| Provider | Detection | URL source |
|
|
70
|
+
|----------|-----------|------------|
|
|
71
|
+
| **Vercel** | `VERCEL` | `VERCEL_PROJECT_PRODUCTION_URL` / `VERCEL_BRANCH_URL` / `VERCEL_URL` |
|
|
72
|
+
| **Netlify** | `NETLIFY` | `URL` / `DEPLOY_PRIME_URL` / `DEPLOY_URL` |
|
|
73
|
+
| **Cloudflare Pages** | `CF_PAGES` | `CF_PAGES_URL` |
|
|
74
|
+
| **Railway** | `RAILWAY_PUBLIC_DOMAIN` | `RAILWAY_PUBLIC_DOMAIN` |
|
|
75
|
+
| **Fly.io** | `FLY_APP_NAME` | `{app}.fly.dev` |
|
|
76
|
+
| **Render** | `RENDER` | `RENDER_EXTERNAL_URL` |
|
|
77
|
+
| **DigitalOcean** | `DIGITALOCEAN_APP_PLATFORM` | `APP_URL` |
|
|
78
|
+
| **Heroku** | `HEROKU_APP_NAME` | `{app}.herokuapp.com` |
|
|
79
|
+
|
|
80
|
+
## Override with `APP_URL`
|
|
81
|
+
|
|
82
|
+
Set `APP_URL` to override auto-detection. Useful for custom domains, tunnels, or unsupported providers.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# .env.local
|
|
86
|
+
APP_URL=https://myapp.com
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`NEXT_PUBLIC_APP_URL` also works (for client-side access in Next.js).
|
|
90
|
+
|
|
91
|
+
## Local development
|
|
92
|
+
|
|
93
|
+
Zero config — auto-detects `http://localhost:3000` (or `PORT` if set).
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Custom port
|
|
97
|
+
APP_URL=http://localhost:4000
|
|
98
|
+
|
|
99
|
+
# Custom local domain
|
|
100
|
+
APP_URL=http://myapp.local:3000
|
|
101
|
+
|
|
102
|
+
# Local HTTPS (mkcert)
|
|
103
|
+
APP_URL=https://localhost:3000
|
|
104
|
+
|
|
105
|
+
# Tunnel
|
|
106
|
+
APP_URL=https://abc123.ngrok-free.app
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For tunnels with dynamic URLs:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
APP_URL=$(curl -s localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url') npm run dev
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Error handling
|
|
116
|
+
|
|
117
|
+
In **production**, `which-url` throws if it can't detect the URL — preventing silent misconfiguration (broken OAuth, CORS, emails pointing to localhost).
|
|
118
|
+
|
|
119
|
+
Use `createUrl` with a fallback to opt into lenient behavior:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { createUrl } from 'which-url'
|
|
123
|
+
|
|
124
|
+
const url = createUrl({ fallback: 'https://fallback.example.com' })
|
|
125
|
+
url.href // never throws
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
In **development**, it always falls back to localhost.
|
|
129
|
+
|
|
130
|
+
## Cloudflare Workers
|
|
131
|
+
|
|
132
|
+
Cloudflare Workers use runtime `env` bindings instead of `process.env`. Set `APP_URL` in `wrangler.toml`:
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
[vars]
|
|
136
|
+
APP_URL = "https://myapp.workers.dev"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Modern wrangler polyfills `process.env` from `[vars]`, so `which-url` picks it up automatically. Cloudflare Pages build-time env vars also work.
|
|
140
|
+
|
|
141
|
+
## Integrations
|
|
142
|
+
|
|
143
|
+
### Better Auth
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { href } from 'which-url'
|
|
147
|
+
import { betterAuth } from 'better-auth'
|
|
148
|
+
|
|
149
|
+
export const auth = betterAuth({
|
|
150
|
+
baseURL: href,
|
|
151
|
+
// ...
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### NextAuth / Auth.js
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { href } from 'which-url'
|
|
159
|
+
|
|
160
|
+
// No need to set NEXTAUTH_URL manually
|
|
161
|
+
process.env.NEXTAUTH_URL = href
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## API
|
|
165
|
+
|
|
166
|
+
### Named exports
|
|
167
|
+
|
|
168
|
+
| Export | Type | Description |
|
|
169
|
+
|--------|------|-------------|
|
|
170
|
+
| `href` | `string` | Full URL (`https://myapp.vercel.app`) |
|
|
171
|
+
| `origin` | `string` | Origin (`https://myapp.vercel.app`) |
|
|
172
|
+
| `hostname` | `string` | Hostname (`myapp.vercel.app`) |
|
|
173
|
+
| `host` | `string` | Host with port (`myapp.vercel.app`) |
|
|
174
|
+
| `protocol` | `string` | Protocol (`https:`) |
|
|
175
|
+
| `port` | `string` | Port (empty if default) |
|
|
176
|
+
| `env` | `AppEnv` | `"production"` \| `"preview"` \| `"local"` |
|
|
177
|
+
| `isProduction` | `boolean` | `true` if production |
|
|
178
|
+
| `isPreview` | `boolean` | `true` if preview/staging |
|
|
179
|
+
| `isLocal` | `boolean` | `true` if local development |
|
|
180
|
+
| `createUrl(options?)` | `function` | Create a new instance with custom options |
|
|
181
|
+
|
|
182
|
+
### `createUrl(options?)`
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
createUrl({ fallback?: string }): WhichUrl
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Re-resolves the URL from current environment. Use for:
|
|
189
|
+
- Custom fallbacks (never throws)
|
|
190
|
+
- Re-resolution in tests
|
|
191
|
+
- Dynamic configuration
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAIrC,wBAAgB,UAAU,IAAI,MAAM,CAuBnC"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
6
|
+
var __toCommonJS = (from) => {
|
|
7
|
+
var entry = __moduleCache.get(from), desc;
|
|
8
|
+
if (entry)
|
|
9
|
+
return entry;
|
|
10
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
12
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
13
|
+
get: () => from[key],
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
}));
|
|
16
|
+
__moduleCache.set(from, entry);
|
|
17
|
+
return entry;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
30
|
+
var exports_src = {};
|
|
31
|
+
__export(exports_src, {
|
|
32
|
+
protocol: () => protocol,
|
|
33
|
+
port: () => port,
|
|
34
|
+
origin: () => origin,
|
|
35
|
+
isProduction: () => isProduction,
|
|
36
|
+
isPreview: () => isPreview,
|
|
37
|
+
isLocal: () => isLocal,
|
|
38
|
+
href: () => href,
|
|
39
|
+
hostname: () => hostname,
|
|
40
|
+
host: () => host,
|
|
41
|
+
env: () => env,
|
|
42
|
+
default: () => src_default,
|
|
43
|
+
createUrl: () => createUrl
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(exports_src);
|
|
46
|
+
|
|
47
|
+
// src/providers.ts
|
|
48
|
+
var providers = [
|
|
49
|
+
{
|
|
50
|
+
name: "vercel",
|
|
51
|
+
detect: (env) => env.VERCEL === "1",
|
|
52
|
+
resolveUrl: (env) => {
|
|
53
|
+
if (env.VERCEL_ENV === "production") {
|
|
54
|
+
return env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL ?? null;
|
|
55
|
+
}
|
|
56
|
+
return env.VERCEL_BRANCH_URL ?? env.VERCEL_URL ?? null;
|
|
57
|
+
},
|
|
58
|
+
resolveEnv: (env) => {
|
|
59
|
+
if (env.VERCEL_ENV === "production")
|
|
60
|
+
return "production";
|
|
61
|
+
if (env.VERCEL_ENV === "preview")
|
|
62
|
+
return "preview";
|
|
63
|
+
return "local";
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "netlify",
|
|
68
|
+
detect: (env) => env.NETLIFY === "true",
|
|
69
|
+
resolveUrl: (env) => {
|
|
70
|
+
if (env.CONTEXT === "production")
|
|
71
|
+
return env.URL ?? null;
|
|
72
|
+
return env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL ?? null;
|
|
73
|
+
},
|
|
74
|
+
resolveEnv: (env) => {
|
|
75
|
+
if (env.CONTEXT === "production")
|
|
76
|
+
return "production";
|
|
77
|
+
if (env.CONTEXT === "deploy-preview" || env.CONTEXT === "branch-deploy")
|
|
78
|
+
return "preview";
|
|
79
|
+
return "local";
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "cloudflare",
|
|
84
|
+
detect: (env) => env.CF_PAGES === "1",
|
|
85
|
+
resolveUrl: (env) => env.CF_PAGES_URL ?? null,
|
|
86
|
+
resolveEnv: (env) => {
|
|
87
|
+
if (env.CF_PAGES_BRANCH === "main" || env.CF_PAGES_BRANCH === "master")
|
|
88
|
+
return "production";
|
|
89
|
+
return "preview";
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "railway",
|
|
94
|
+
detect: (env) => !!env.RAILWAY_PUBLIC_DOMAIN,
|
|
95
|
+
resolveUrl: (env) => env.RAILWAY_PUBLIC_DOMAIN ?? null,
|
|
96
|
+
resolveEnv: (env) => {
|
|
97
|
+
if (env.RAILWAY_ENVIRONMENT === "production")
|
|
98
|
+
return "production";
|
|
99
|
+
return "preview";
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "render",
|
|
104
|
+
detect: (env) => env.RENDER === "true",
|
|
105
|
+
resolveUrl: (env) => env.RENDER_EXTERNAL_URL ?? null,
|
|
106
|
+
resolveEnv: (env) => {
|
|
107
|
+
if (env.IS_PULL_REQUEST === "true")
|
|
108
|
+
return "preview";
|
|
109
|
+
return "production";
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "fly",
|
|
114
|
+
detect: (env) => !!env.FLY_APP_NAME,
|
|
115
|
+
resolveUrl: (env) => {
|
|
116
|
+
const name = env.FLY_APP_NAME;
|
|
117
|
+
return name ? `${name}.fly.dev` : null;
|
|
118
|
+
},
|
|
119
|
+
resolveEnv: () => "production"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "digitalocean",
|
|
123
|
+
detect: (env) => !!env.DIGITALOCEAN_APP_PLATFORM,
|
|
124
|
+
resolveUrl: (env) => env.APP_URL ?? null,
|
|
125
|
+
resolveEnv: () => "production"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "heroku",
|
|
129
|
+
detect: (env) => !!env.HEROKU_APP_NAME,
|
|
130
|
+
resolveUrl: (env) => {
|
|
131
|
+
const name = env.HEROKU_APP_NAME;
|
|
132
|
+
return name ? `${name}.herokuapp.com` : null;
|
|
133
|
+
},
|
|
134
|
+
resolveEnv: () => "production"
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// src/normalize.ts
|
|
139
|
+
function normalizeUrl(raw, protocol = "https") {
|
|
140
|
+
let url = raw.trim();
|
|
141
|
+
url = url.replace(/\/+$/, "");
|
|
142
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
143
|
+
return url;
|
|
144
|
+
}
|
|
145
|
+
url = url.replace(/^\/+/, "");
|
|
146
|
+
return `${protocol}://${url}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/resolve.ts
|
|
150
|
+
function resolveUrl(options) {
|
|
151
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
152
|
+
const override = env.APP_URL ?? env.NEXT_PUBLIC_APP_URL;
|
|
153
|
+
if (override)
|
|
154
|
+
return normalizeUrl(override);
|
|
155
|
+
for (const p of providers) {
|
|
156
|
+
if (p.detect(env)) {
|
|
157
|
+
const url = p.resolveUrl(env);
|
|
158
|
+
if (url)
|
|
159
|
+
return normalizeUrl(url);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (typeof window !== "undefined" && window.location?.origin) {
|
|
163
|
+
return window.location.origin;
|
|
164
|
+
}
|
|
165
|
+
if (options?.fallback)
|
|
166
|
+
return normalizeUrl(options.fallback);
|
|
167
|
+
const isProduction = env.NODE_ENV === "production";
|
|
168
|
+
if (isProduction) {
|
|
169
|
+
throw new Error(`thisapp: Cannot detect app URL. Set APP_URL environment variable.
|
|
170
|
+
` + "See: https://github.com/manishrc/thisapp#configuration");
|
|
171
|
+
}
|
|
172
|
+
const port = env.PORT ?? "3000";
|
|
173
|
+
return `http://localhost:${port}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/env.ts
|
|
177
|
+
function resolveEnv() {
|
|
178
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
179
|
+
const override = env.APP_ENV;
|
|
180
|
+
if (override === "production" || override === "preview" || override === "local") {
|
|
181
|
+
return override;
|
|
182
|
+
}
|
|
183
|
+
if (env.NODE_ENV === "development")
|
|
184
|
+
return "local";
|
|
185
|
+
for (const p of providers) {
|
|
186
|
+
if (p.detect(env)) {
|
|
187
|
+
return p.resolveEnv(env);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (env.NODE_ENV === "production")
|
|
191
|
+
return "production";
|
|
192
|
+
return "local";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/index.ts
|
|
196
|
+
function createUrl(options) {
|
|
197
|
+
const resolved = resolveUrl(options);
|
|
198
|
+
const parsed = new URL(resolved);
|
|
199
|
+
const env = resolveEnv();
|
|
200
|
+
return {
|
|
201
|
+
href: parsed.origin,
|
|
202
|
+
origin: parsed.origin,
|
|
203
|
+
hostname: parsed.hostname,
|
|
204
|
+
host: parsed.host,
|
|
205
|
+
protocol: parsed.protocol,
|
|
206
|
+
port: parsed.port,
|
|
207
|
+
env,
|
|
208
|
+
isProduction: env === "production",
|
|
209
|
+
isPreview: env === "preview",
|
|
210
|
+
isLocal: env === "local"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var _resolved = createUrl();
|
|
214
|
+
var href = _resolved.href;
|
|
215
|
+
var origin = _resolved.origin;
|
|
216
|
+
var hostname = _resolved.hostname;
|
|
217
|
+
var host = _resolved.host;
|
|
218
|
+
var protocol = _resolved.protocol;
|
|
219
|
+
var port = _resolved.port;
|
|
220
|
+
var env = _resolved.env;
|
|
221
|
+
var isProduction = _resolved.isProduction;
|
|
222
|
+
var isPreview = _resolved.isPreview;
|
|
223
|
+
var isLocal = _resolved.isLocal;
|
|
224
|
+
var src_default = _resolved;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WhichUrl, AppEnv, CreateUrlOptions } from "./types";
|
|
2
|
+
export declare function createUrl(options?: CreateUrlOptions): WhichUrl;
|
|
3
|
+
declare const _resolved: WhichUrl;
|
|
4
|
+
export declare const href: string;
|
|
5
|
+
export declare const origin: string;
|
|
6
|
+
export declare const hostname: string;
|
|
7
|
+
export declare const host: string;
|
|
8
|
+
export declare const protocol: string;
|
|
9
|
+
export declare const port: string;
|
|
10
|
+
export declare const env: AppEnv;
|
|
11
|
+
export declare const isProduction: boolean;
|
|
12
|
+
export declare const isPreview: boolean;
|
|
13
|
+
export declare const isLocal: boolean;
|
|
14
|
+
export default _resolved;
|
|
15
|
+
export type { WhichUrl, AppEnv, CreateUrlOptions };
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAEjE,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,QAAQ,CAgB9D;AAGD,QAAA,MAAM,SAAS,UAAc,CAAA;AAG7B,eAAO,MAAM,IAAI,EAAE,MAAuB,CAAA;AAC1C,eAAO,MAAM,MAAM,EAAE,MAAyB,CAAA;AAC9C,eAAO,MAAM,QAAQ,EAAE,MAA2B,CAAA;AAClD,eAAO,MAAM,IAAI,EAAE,MAAuB,CAAA;AAC1C,eAAO,MAAM,QAAQ,EAAE,MAA2B,CAAA;AAClD,eAAO,MAAM,IAAI,EAAE,MAAuB,CAAA;AAC1C,eAAO,MAAM,GAAG,EAAE,MAAsB,CAAA;AACxC,eAAO,MAAM,YAAY,EAAE,OAAgC,CAAA;AAC3D,eAAO,MAAM,SAAS,EAAE,OAA6B,CAAA;AACrD,eAAO,MAAM,OAAO,EAAE,OAA2B,CAAA;AAGjD,eAAe,SAAS,CAAA;AAExB,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// src/providers.ts
|
|
2
|
+
var providers = [
|
|
3
|
+
{
|
|
4
|
+
name: "vercel",
|
|
5
|
+
detect: (env) => !!env.VERCEL,
|
|
6
|
+
resolveUrl: (env) => {
|
|
7
|
+
if (env.VERCEL_ENV === "production") {
|
|
8
|
+
return env.VERCEL_PROJECT_PRODUCTION_URL || env.VERCEL_URL || null;
|
|
9
|
+
}
|
|
10
|
+
return env.VERCEL_BRANCH_URL || env.VERCEL_URL || null;
|
|
11
|
+
},
|
|
12
|
+
resolveEnv: (env) => {
|
|
13
|
+
if (env.VERCEL_ENV === "production")
|
|
14
|
+
return "production";
|
|
15
|
+
if (env.VERCEL_ENV === "preview")
|
|
16
|
+
return "preview";
|
|
17
|
+
return "local";
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "netlify",
|
|
22
|
+
detect: (env) => !!env.NETLIFY,
|
|
23
|
+
resolveUrl: (env) => {
|
|
24
|
+
if (env.CONTEXT === "production")
|
|
25
|
+
return env.URL || null;
|
|
26
|
+
return env.DEPLOY_PRIME_URL || env.DEPLOY_URL || null;
|
|
27
|
+
},
|
|
28
|
+
resolveEnv: (env) => {
|
|
29
|
+
if (env.CONTEXT === "production")
|
|
30
|
+
return "production";
|
|
31
|
+
if (env.CONTEXT === "deploy-preview" || env.CONTEXT === "branch-deploy")
|
|
32
|
+
return "preview";
|
|
33
|
+
return "local";
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "cloudflare",
|
|
38
|
+
detect: (env) => !!env.CF_PAGES,
|
|
39
|
+
resolveUrl: (env) => env.CF_PAGES_URL || null,
|
|
40
|
+
resolveEnv: (env) => {
|
|
41
|
+
if (env.CF_PAGES_BRANCH === "main" || env.CF_PAGES_BRANCH === "master")
|
|
42
|
+
return "production";
|
|
43
|
+
return "preview";
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "railway",
|
|
48
|
+
detect: (env) => !!env.RAILWAY_PUBLIC_DOMAIN,
|
|
49
|
+
resolveUrl: (env) => env.RAILWAY_PUBLIC_DOMAIN || null,
|
|
50
|
+
resolveEnv: (env) => {
|
|
51
|
+
if (env.RAILWAY_ENVIRONMENT === "production")
|
|
52
|
+
return "production";
|
|
53
|
+
return "production";
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "fly",
|
|
58
|
+
detect: (env) => !!env.FLY_APP_NAME,
|
|
59
|
+
resolveUrl: (env) => env.FLY_APP_NAME ? `${env.FLY_APP_NAME}.fly.dev` : null,
|
|
60
|
+
resolveEnv: () => "production"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "render",
|
|
64
|
+
detect: (env) => !!env.RENDER,
|
|
65
|
+
resolveUrl: (env) => env.RENDER_EXTERNAL_URL || null,
|
|
66
|
+
resolveEnv: (env) => {
|
|
67
|
+
if (env.IS_PULL_REQUEST === "true")
|
|
68
|
+
return "preview";
|
|
69
|
+
return "production";
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "digitalocean",
|
|
74
|
+
detect: (env) => !!env.DIGITALOCEAN_APP_PLATFORM,
|
|
75
|
+
resolveUrl: (env) => env.APP_URL || null,
|
|
76
|
+
resolveEnv: () => "production"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "heroku",
|
|
80
|
+
detect: (env) => !!env.HEROKU_APP_NAME,
|
|
81
|
+
resolveUrl: (env) => env.HEROKU_APP_NAME ? `${env.HEROKU_APP_NAME}.herokuapp.com` : null,
|
|
82
|
+
resolveEnv: () => "production"
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// src/normalize.ts
|
|
87
|
+
function normalizeUrl(raw) {
|
|
88
|
+
let url = raw.trim().replace(/\/+$/, "");
|
|
89
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
90
|
+
return url;
|
|
91
|
+
}
|
|
92
|
+
return `https://${url}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/resolve.ts
|
|
96
|
+
function resolveUrl(options) {
|
|
97
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
98
|
+
const override = env.APP_URL || env.NEXT_PUBLIC_APP_URL;
|
|
99
|
+
if (override)
|
|
100
|
+
return normalizeUrl(override);
|
|
101
|
+
for (const p of providers) {
|
|
102
|
+
if (p.detect(env)) {
|
|
103
|
+
const url = p.resolveUrl(env);
|
|
104
|
+
if (url)
|
|
105
|
+
return normalizeUrl(url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (typeof window !== "undefined" && window.location) {
|
|
109
|
+
return window.location.origin;
|
|
110
|
+
}
|
|
111
|
+
const isProduction = env.NODE_ENV === "production";
|
|
112
|
+
if (!isProduction) {
|
|
113
|
+
const port = env.PORT || "3000";
|
|
114
|
+
return `http://localhost:${port}`;
|
|
115
|
+
}
|
|
116
|
+
if (options?.fallback) {
|
|
117
|
+
return normalizeUrl(options.fallback);
|
|
118
|
+
}
|
|
119
|
+
throw new Error("which-url: Cannot detect app URL. Set APP_URL environment variable.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/env.ts
|
|
123
|
+
var validEnvs = ["production", "preview", "local"];
|
|
124
|
+
function resolveEnv() {
|
|
125
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
126
|
+
if (env.APP_ENV && validEnvs.includes(env.APP_ENV)) {
|
|
127
|
+
return env.APP_ENV;
|
|
128
|
+
}
|
|
129
|
+
if (env.NODE_ENV === "development")
|
|
130
|
+
return "local";
|
|
131
|
+
for (const p of providers) {
|
|
132
|
+
if (p.detect(env)) {
|
|
133
|
+
return p.resolveEnv(env);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (env.NODE_ENV === "production")
|
|
137
|
+
return "production";
|
|
138
|
+
return "local";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/index.ts
|
|
142
|
+
function createUrl(options) {
|
|
143
|
+
const resolved = resolveUrl(options);
|
|
144
|
+
const parsed = new URL(resolved);
|
|
145
|
+
const env = resolveEnv();
|
|
146
|
+
return {
|
|
147
|
+
href: parsed.origin,
|
|
148
|
+
origin: parsed.origin,
|
|
149
|
+
hostname: parsed.hostname,
|
|
150
|
+
host: parsed.host,
|
|
151
|
+
protocol: parsed.protocol,
|
|
152
|
+
port: parsed.port,
|
|
153
|
+
env,
|
|
154
|
+
isProduction: env === "production",
|
|
155
|
+
isPreview: env === "preview",
|
|
156
|
+
isLocal: env === "local"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var _resolved = createUrl();
|
|
160
|
+
var href = _resolved.href;
|
|
161
|
+
var origin = _resolved.origin;
|
|
162
|
+
var hostname = _resolved.hostname;
|
|
163
|
+
var host = _resolved.host;
|
|
164
|
+
var protocol = _resolved.protocol;
|
|
165
|
+
var port = _resolved.port;
|
|
166
|
+
var env = _resolved.env;
|
|
167
|
+
var isProduction = _resolved.isProduction;
|
|
168
|
+
var isPreview = _resolved.isPreview;
|
|
169
|
+
var isLocal = _resolved.isLocal;
|
|
170
|
+
var src_default = _resolved;
|
|
171
|
+
export {
|
|
172
|
+
protocol,
|
|
173
|
+
port,
|
|
174
|
+
origin,
|
|
175
|
+
isProduction,
|
|
176
|
+
isPreview,
|
|
177
|
+
isLocal,
|
|
178
|
+
href,
|
|
179
|
+
hostname,
|
|
180
|
+
host,
|
|
181
|
+
env,
|
|
182
|
+
src_default as default,
|
|
183
|
+
createUrl
|
|
184
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMhD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,eAAO,MAAM,SAAS,EAAE,gBAAgB,EA8EvC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAoC7D"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type AppEnv = "production" | "preview" | "local";
|
|
2
|
+
export interface WhichUrl {
|
|
3
|
+
href: string;
|
|
4
|
+
origin: string;
|
|
5
|
+
hostname: string;
|
|
6
|
+
host: string;
|
|
7
|
+
protocol: string;
|
|
8
|
+
port: string;
|
|
9
|
+
env: AppEnv;
|
|
10
|
+
isProduction: boolean;
|
|
11
|
+
isPreview: boolean;
|
|
12
|
+
isLocal: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateUrlOptions {
|
|
15
|
+
fallback?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ProviderDetector {
|
|
18
|
+
name: string;
|
|
19
|
+
detect: (env: Record<string, string | undefined>) => boolean;
|
|
20
|
+
resolveUrl: (env: Record<string, string | undefined>) => string | null;
|
|
21
|
+
resolveEnv: (env: Record<string, string | undefined>) => AppEnv;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,CAAA;AAEvD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAK,OAAO,CAAA;IAC5D,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IACtE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAK,MAAM,CAAA;CAChE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "which-url",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Auto-detect your app's URL across hosting providers. Zero config.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"keywords": ["url", "base-url", "vercel", "netlify", "cloudflare", "railway", "hosting", "auto-detect", "which-url"],
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "manishrc",
|
|
20
|
+
"url": "https://manishrc.com"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/manishrc/which-url"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "bun build src/index.ts --outdir dist --format esm && tsc --emitDeclarationOnly --outDir dist",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"prepublishOnly": "bun run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
34
|
+
"typescript": "^5.8.0"
|
|
35
|
+
}
|
|
36
|
+
}
|