@dbx-tools/shared 0.1.18
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 +234 -0
- package/dist/index.client.d.ts +32 -0
- package/dist/index.client.js +32 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/src/api.d.ts +90 -0
- package/dist/src/api.js +165 -0
- package/dist/src/appkit.d.ts +59 -0
- package/dist/src/appkit.js +109 -0
- package/dist/src/common.d.ts +185 -0
- package/dist/src/common.js +277 -0
- package/dist/src/http.d.ts +77 -0
- package/dist/src/http.js +166 -0
- package/dist/src/log.d.ts +47 -0
- package/dist/src/log.js +80 -0
- package/dist/src/net.browser.d.ts +98 -0
- package/dist/src/net.browser.js +146 -0
- package/dist/src/net.d.ts +14 -0
- package/dist/src/net.js +29 -0
- package/dist/src/project.d.ts +33 -0
- package/dist/src/project.js +215 -0
- package/dist/src/string.d.ts +105 -0
- package/dist/src/string.js +220 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.client.ts +32 -0
- package/index.ts +26 -0
- package/package.json +54 -0
- package/src/api.ts +222 -0
- package/src/appkit.ts +161 -0
- package/src/common.ts +422 -0
- package/src/http.ts +203 -0
- package/src/log.ts +116 -0
- package/src/net.browser.ts +174 -0
- package/src/net.ts +32 -0
- package/src/project.ts +264 -0
- package/src/string.ts +276 -0
package/dist/src/http.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP header helpers shared across AppKit plugins: framework
|
|
3
|
+
* agnostic readers for HTTP headers and cookies that work uniformly
|
|
4
|
+
* across Express, Node `IncomingMessage`, WHATWG `Request` / `Response`
|
|
5
|
+
* / `Headers`, Hono, and any object that exposes a `headers` field of
|
|
6
|
+
* one of those shapes.
|
|
7
|
+
*
|
|
8
|
+
* Public API: {@link forEachHeaderValue}, {@link parseCookies}.
|
|
9
|
+
* Everything else (the header guards `isHeaders` / `isWrapped` /
|
|
10
|
+
* `unwrap`, the single cookie-header parser `parseCookieString`) is
|
|
11
|
+
* private to this module.
|
|
12
|
+
*
|
|
13
|
+
* URL parsing and path joining moved to `netUtils` (`./net.browser.ts`)
|
|
14
|
+
* so the URL surface lives next to the rest of the browser-safe
|
|
15
|
+
* networking helpers. The Databricks-aware REST helper that used to
|
|
16
|
+
* live here moved to `apiUtils.fetchApi` (`./api.ts`) so this module
|
|
17
|
+
* can stay dependency-free and browser-safe.
|
|
18
|
+
*/
|
|
19
|
+
// ────────────────────────────────────────────────────────────────
|
|
20
|
+
// Header helpers
|
|
21
|
+
// ────────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Invokes `consumer` once per value for `headerName`, case-insensitive.
|
|
24
|
+
*
|
|
25
|
+
* - **Record input:** if the field is an array (e.g. repeated `Set-Cookie`),
|
|
26
|
+
* `consumer` runs once per array item.
|
|
27
|
+
* - **`Headers` input:** uses `get(name)` (which spec-joins repeats with
|
|
28
|
+
* `, `) except for `Set-Cookie`, which uses `getSetCookie()` so each
|
|
29
|
+
* cookie is delivered separately.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* forEachHeaderValue(req, "x-trace-id", (v) => spans.push(v)); // Express
|
|
33
|
+
* forEachHeaderValue(c.req.raw, "set-cookie", (v) => log(v)); // Hono
|
|
34
|
+
* forEachHeaderValue(headersInstance, "cookie", parse); // fetch
|
|
35
|
+
*/
|
|
36
|
+
export function forEachHeaderValue(input, headerName, consumer) {
|
|
37
|
+
const headers = unwrap(input);
|
|
38
|
+
if (!headers)
|
|
39
|
+
return;
|
|
40
|
+
const target = headerName.toLowerCase();
|
|
41
|
+
if (isHeaders(headers)) {
|
|
42
|
+
// `Headers.get` joins repeated values with `, ` per spec, which
|
|
43
|
+
// mangles `Set-Cookie` (cookies legitimately contain commas in
|
|
44
|
+
// their `expires=` attribute). `getSetCookie` is the dedicated
|
|
45
|
+
// splitter and is the only safe path for that header.
|
|
46
|
+
if (target === "set-cookie") {
|
|
47
|
+
for (const value of headers.getSetCookie())
|
|
48
|
+
consumer(value);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const value = headers.get(headerName);
|
|
52
|
+
if (value !== null)
|
|
53
|
+
consumer(value);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
57
|
+
if (value == null || key.toLowerCase() !== target)
|
|
58
|
+
continue;
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
for (const item of value)
|
|
61
|
+
consumer(item);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
consumer(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parses `Cookie` header values into a name-to-value map (URI-decoded).
|
|
70
|
+
*
|
|
71
|
+
* Accepts:
|
|
72
|
+
*
|
|
73
|
+
* - A raw `Cookie` string (`"a=1; b=2"`).
|
|
74
|
+
* - An array of such strings (e.g. multiple `Cookie` headers).
|
|
75
|
+
* - Any {@link HeaderLike}: a WHATWG `Headers` instance, a header
|
|
76
|
+
* record, or a request-like object with a `headers` field.
|
|
77
|
+
*
|
|
78
|
+
* First occurrence of each cookie name wins; later duplicates are ignored.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* parseCookies("session=abc; theme=dark");
|
|
82
|
+
* // { session: "abc", theme: "dark" }
|
|
83
|
+
*
|
|
84
|
+
* parseCookies(req); // Express / Node
|
|
85
|
+
* parseCookies(c.req.raw); // Hono
|
|
86
|
+
* parseCookies(request); // fetch Request
|
|
87
|
+
* parseCookies(request.headers); // WHATWG Headers directly
|
|
88
|
+
*/
|
|
89
|
+
export function parseCookies(input) {
|
|
90
|
+
if (input == null)
|
|
91
|
+
return {};
|
|
92
|
+
const out = {};
|
|
93
|
+
if (typeof input === "string") {
|
|
94
|
+
parseCookieString(input, out);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(input)) {
|
|
98
|
+
for (const item of input) {
|
|
99
|
+
if (typeof item === "string")
|
|
100
|
+
parseCookieString(item, out);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
forEachHeaderValue(input, "cookie", (value) => {
|
|
105
|
+
parseCookieString(value, out);
|
|
106
|
+
});
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
// ────────────────────────────────────────────────────────────────
|
|
110
|
+
// Private helpers
|
|
111
|
+
// ────────────────────────────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Type guard for WHATWG `Headers`. Duck-types on the two methods that
|
|
114
|
+
* matter to this module (`get` and `getSetCookie`) so polyfilled
|
|
115
|
+
* implementations and Hono's `HonoHeaders` are accepted without
|
|
116
|
+
* pulling `Headers` in as a hard dependency.
|
|
117
|
+
*/
|
|
118
|
+
function isHeaders(value) {
|
|
119
|
+
return (typeof value === "object" &&
|
|
120
|
+
value !== null &&
|
|
121
|
+
typeof value.get === "function" &&
|
|
122
|
+
typeof value.getSetCookie === "function");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `HeaderRecord` is an index signature, so `"headers" in input` cannot
|
|
126
|
+
* discriminate it from the wrapped `{ headers }` shape at the type
|
|
127
|
+
* level. This guard inspects the runtime value of `headers`: only
|
|
128
|
+
* objects (`Headers` or a nested record) qualify as the wrapper shape,
|
|
129
|
+
* never stray string/array values that happen to live under a `headers`
|
|
130
|
+
* key on a header record.
|
|
131
|
+
*/
|
|
132
|
+
function isWrapped(input) {
|
|
133
|
+
const headers = input.headers;
|
|
134
|
+
return headers != null && typeof headers === "object" && !Array.isArray(headers);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parse a single `Cookie`-style header string (`"a=1; b=2"`) into
|
|
138
|
+
* `out`. Names without a value are skipped; first occurrence wins so
|
|
139
|
+
* later duplicates are ignored. Cookie values are URI-decoded.
|
|
140
|
+
*/
|
|
141
|
+
function parseCookieString(input, out) {
|
|
142
|
+
for (const part of input.split(";")) {
|
|
143
|
+
const eq = part.indexOf("=");
|
|
144
|
+
if (eq < 0)
|
|
145
|
+
continue;
|
|
146
|
+
const name = part.slice(0, eq).trim();
|
|
147
|
+
if (!name || name in out)
|
|
148
|
+
continue;
|
|
149
|
+
const raw = part.slice(eq + 1).trim();
|
|
150
|
+
out[name] = decodeURIComponent(raw);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a {@link HeaderLike} input down to either a `Headers`
|
|
155
|
+
* instance or a header `Record`. Returns `null` for missing input so
|
|
156
|
+
* callers can short-circuit without a separate nullish check.
|
|
157
|
+
*/
|
|
158
|
+
function unwrap(input) {
|
|
159
|
+
if (input == null)
|
|
160
|
+
return null;
|
|
161
|
+
if (isHeaders(input))
|
|
162
|
+
return input;
|
|
163
|
+
if (isWrapped(input))
|
|
164
|
+
return input.headers;
|
|
165
|
+
return input;
|
|
166
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { NameLike } from "./common.js";
|
|
2
|
+
/** Plugin-facing logger surface returned by {@link logger}. */
|
|
3
|
+
export interface Logger {
|
|
4
|
+
debug(message: string, attributes?: Record<string, unknown>): void;
|
|
5
|
+
info(message: string, attributes?: Record<string, unknown>): void;
|
|
6
|
+
warn(message: string, attributes?: Record<string, unknown>): void;
|
|
7
|
+
error(message: string, attributes?: Record<string, unknown>): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Severity ordering. A log call below the active threshold is
|
|
11
|
+
* discarded entirely (no string formatting, no console call). The
|
|
12
|
+
* threshold is read on every call from `process.env.LOG_LEVEL`,
|
|
13
|
+
* case-insensitive, defaulting to `info` when unset / empty /
|
|
14
|
+
* unrecognised. Set `LOG_LEVEL=debug` for verbose dev output,
|
|
15
|
+
* `LOG_LEVEL=warn` to silence info chatter in production, etc.
|
|
16
|
+
*
|
|
17
|
+
* Lazy on purpose: looking up `process.env.LOG_LEVEL` per call
|
|
18
|
+
* costs nothing meaningful, and it lets test runners (and other
|
|
19
|
+
* embedders) flip the level after `log.ts` has already been
|
|
20
|
+
* imported, without restarting the process or reaching into
|
|
21
|
+
* private state.
|
|
22
|
+
*/
|
|
23
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
24
|
+
/**
|
|
25
|
+
* Build a per-plugin logger that writes to `console` with an optional
|
|
26
|
+
* `[plugin-name]` prefix derived from `plugin.name` or the string you pass in.
|
|
27
|
+
*
|
|
28
|
+
* Calls below `process.env.LOG_LEVEL` are discarded before any
|
|
29
|
+
* string work happens, so leaving `log.debug({...heavy details})`
|
|
30
|
+
* in production code is free as long as `LOG_LEVEL` is `info` or
|
|
31
|
+
* higher. Default level is `info`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { logUtils } from "@dbx-tools/shared";
|
|
36
|
+
*
|
|
37
|
+
* class MyPlugin extends Plugin {
|
|
38
|
+
* private log = logUtils.logger(this);
|
|
39
|
+
*
|
|
40
|
+
* override async setup() {
|
|
41
|
+
* this.log.info("starting");
|
|
42
|
+
* this.log.warn("missing optional config", { reason: "no env var" });
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function logger(loggerName: NameLike | string | undefined): Logger;
|
package/dist/src/log.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const LEVEL_RANK = {
|
|
2
|
+
debug: 10,
|
|
3
|
+
info: 20,
|
|
4
|
+
warn: 30,
|
|
5
|
+
error: 40,
|
|
6
|
+
};
|
|
7
|
+
const DEFAULT_LEVEL = "info";
|
|
8
|
+
/**
|
|
9
|
+
* Read the active threshold from `process.env.LOG_LEVEL`. Recognises
|
|
10
|
+
* any case (`DEBUG`, `Debug`, `debug`), trims whitespace, falls back
|
|
11
|
+
* to {@link DEFAULT_LEVEL} when unset / empty / unrecognised.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe: `process` is undefined in browser bundles unless a
|
|
14
|
+
* polyfill or build-time replace is set up, so we guard the access.
|
|
15
|
+
* In a Vite app, set `LOG_LEVEL` via `define` config or just leave
|
|
16
|
+
* it - the default `info` is sane for production browser code.
|
|
17
|
+
*/
|
|
18
|
+
function activeLevel() {
|
|
19
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
20
|
+
const raw = env?.LOG_LEVEL?.toLowerCase().trim();
|
|
21
|
+
if (raw && Object.prototype.hasOwnProperty.call(LEVEL_RANK, raw)) {
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
return DEFAULT_LEVEL;
|
|
25
|
+
}
|
|
26
|
+
/** True when calls at `level` should reach the console. */
|
|
27
|
+
function shouldEmit(level) {
|
|
28
|
+
return LEVEL_RANK[level] >= LEVEL_RANK[activeLevel()];
|
|
29
|
+
}
|
|
30
|
+
const LOGGER_NAME_REGEX = /^(?:[a-z][a-z0-9+.-]*:\/\/)?.*\/([^/.]+)(?:\.[^/]+)?$/i;
|
|
31
|
+
function extractLoggerName(loggerName) {
|
|
32
|
+
if (!loggerName)
|
|
33
|
+
return undefined;
|
|
34
|
+
else if (typeof loggerName === "string") {
|
|
35
|
+
const match = loggerName.match(LOGGER_NAME_REGEX);
|
|
36
|
+
return match?.[1] ?? loggerName;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return extractLoggerName(loggerName?.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a per-plugin logger that writes to `console` with an optional
|
|
44
|
+
* `[plugin-name]` prefix derived from `plugin.name` or the string you pass in.
|
|
45
|
+
*
|
|
46
|
+
* Calls below `process.env.LOG_LEVEL` are discarded before any
|
|
47
|
+
* string work happens, so leaving `log.debug({...heavy details})`
|
|
48
|
+
* in production code is free as long as `LOG_LEVEL` is `info` or
|
|
49
|
+
* higher. Default level is `info`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { logUtils } from "@dbx-tools/shared";
|
|
54
|
+
*
|
|
55
|
+
* class MyPlugin extends Plugin {
|
|
56
|
+
* private log = logUtils.logger(this);
|
|
57
|
+
*
|
|
58
|
+
* override async setup() {
|
|
59
|
+
* this.log.info("starting");
|
|
60
|
+
* this.log.warn("missing optional config", { reason: "no env var" });
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function logger(loggerName) {
|
|
66
|
+
const name = extractLoggerName(loggerName);
|
|
67
|
+
function log(level, consoleFn, message, attributes) {
|
|
68
|
+
if (!shouldEmit(level))
|
|
69
|
+
return;
|
|
70
|
+
const logMessage = name ? `[${name}] ${message}` : message;
|
|
71
|
+
const logArgs = [logMessage, attributes].filter(Boolean);
|
|
72
|
+
consoleFn(...logArgs);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
debug: (msg, attrs) => log("debug", console.debug, msg, attrs),
|
|
76
|
+
info: (msg, attrs) => log("info", console.info, msg, attrs),
|
|
77
|
+
warn: (msg, attrs) => log("warn", console.warn, msg, attrs),
|
|
78
|
+
error: (msg, attrs) => log("error", console.error, msg, attrs),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe networking helpers: URL parsing and path joining that
|
|
3
|
+
* gracefully handle partial inputs. No node-only imports, so this
|
|
4
|
+
* module is the canonical home for anything URL-shaped that also has
|
|
5
|
+
* to run in a Vite / Webpack / esbuild client bundle.
|
|
6
|
+
*
|
|
7
|
+
* Public API: {@link joinUrl}, {@link parseUrl}. The
|
|
8
|
+
* single-leading/trailing-slash stripper `stripBoundarySlashes` is
|
|
9
|
+
* private to this module.
|
|
10
|
+
*
|
|
11
|
+
* The server-side `./net.ts` re-exports everything here verbatim and
|
|
12
|
+
* tacks on its own node-only helpers (e.g. `getRandomPort`), so the
|
|
13
|
+
* `netUtils` namespace looks identical from both entry points.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* One input to {@link joinUrl}: a string, a (recursively nested)
|
|
17
|
+
* array of segments, or `null` / `undefined` (skipped).
|
|
18
|
+
*
|
|
19
|
+
* The recursive shape lets callers compose path fragments without
|
|
20
|
+
* pre-flattening: `joinUrl("a", ["b", ["c", "d"]])` is valid.
|
|
21
|
+
*
|
|
22
|
+
* The array variant uses an interface (rather than a self-referential
|
|
23
|
+
* type alias) because Bun's TS parser bails on `type X = ... | X[] | ...`
|
|
24
|
+
* but accepts the equivalent `interface XArr extends Array<X>`.
|
|
25
|
+
*/
|
|
26
|
+
export type UrlSegmentLike = string | UrlSegmentArray | null | undefined;
|
|
27
|
+
export interface UrlSegmentArray extends ReadonlyArray<UrlSegmentLike> {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Anything {@link parseUrl} knows how to coerce into a `URL`:
|
|
31
|
+
*
|
|
32
|
+
* - A WHATWG `URL` instance: returned as-is when no extra path is
|
|
33
|
+
* supplied; otherwise re-parsed with the path appended.
|
|
34
|
+
* - A string: parsed by the `URL` constructor; `https://` is
|
|
35
|
+
* auto-prefixed when no scheme is present, so bare hostnames like
|
|
36
|
+
* `"example.com"` round-trip into a usable URL.
|
|
37
|
+
* - Any object with a `url` field of the above shapes (e.g. a fetch
|
|
38
|
+
* `Request`, a Databricks `WorkspaceClient` config, etc.).
|
|
39
|
+
*/
|
|
40
|
+
export type UrlLike = URL | string | {
|
|
41
|
+
url: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Join URL path segments with `/`, with three pragmatic conveniences
|
|
45
|
+
* for building URLs piecemeal:
|
|
46
|
+
*
|
|
47
|
+
* 1. Nullish or blank segments are dropped, so callers don't have
|
|
48
|
+
* to guard around optional path components.
|
|
49
|
+
* 2. Each string segment has any leading or trailing `/` stripped
|
|
50
|
+
* before joining, so `"/api"` + `"v2/"` round-trips to `/api/v2`
|
|
51
|
+
* regardless of how the caller terminated each part.
|
|
52
|
+
* 3. Array segments recurse, with their joined form inlined into
|
|
53
|
+
* the outer result.
|
|
54
|
+
*
|
|
55
|
+
* The result is prefixed with `/` to make it path-absolute, except
|
|
56
|
+
* when any segment carries an explicit `://` scheme (e.g.
|
|
57
|
+
* `"https://x.com"`), in which case the scheme is preserved verbatim.
|
|
58
|
+
*
|
|
59
|
+
* Returns `""` when every input was nullish or blank.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* joinUrl("a", "b"); // "/a/b"
|
|
63
|
+
* joinUrl("/api/", "/v2/", "x"); // "/api/v2/x"
|
|
64
|
+
* joinUrl(["a", "b"], "c"); // "/a/b/c"
|
|
65
|
+
* joinUrl("https://ex.com", "/api/x"); // "https://ex.com/api/x"
|
|
66
|
+
* joinUrl(null, "", "x", undefined); // "/x"
|
|
67
|
+
* joinUrl(); // ""
|
|
68
|
+
* joinUrl(null); // ""
|
|
69
|
+
*/
|
|
70
|
+
export declare function joinUrl(...urlSegments: UrlSegmentLike[]): string;
|
|
71
|
+
/**
|
|
72
|
+
* Coerce a {@link UrlLike} input into a parsed `URL`, returning `null`
|
|
73
|
+
* for anything that cannot be coerced (null/undefined, empty string,
|
|
74
|
+
* malformed URL, an object whose `url` field doesn't parse).
|
|
75
|
+
*
|
|
76
|
+
* Mirrors WHATWG `URL.parse(...)` semantics: parse on success, `null`
|
|
77
|
+
* on failure - never throws.
|
|
78
|
+
*
|
|
79
|
+
* Bare hostnames are upgraded to `https://` before parsing, so callers
|
|
80
|
+
* can hand in user-provided values like `"workspace.cloud.databricks.com"`
|
|
81
|
+
* without an explicit scheme.
|
|
82
|
+
*
|
|
83
|
+
* Optional trailing `path` arguments are appended via {@link joinUrl}.
|
|
84
|
+
* When `input` is nullish (or just `"/"` / blank) but a `path` is
|
|
85
|
+
* provided, the URL is built against `http://localhost`, which is
|
|
86
|
+
* convenient for tests and for callers that resolve the host later.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* parseUrl("example.com"); // URL { https://example.com/ }
|
|
90
|
+
* parseUrl("http://example.com/path"); // URL { http://example.com/path }
|
|
91
|
+
* parseUrl({ url: "https://api.example" }); // URL { https://api.example/ }
|
|
92
|
+
* parseUrl("example.com", "/api", "v2", "items"); // URL { https://example.com/api/v2/items }
|
|
93
|
+
* parseUrl("example.com", ["api", "v2"]); // URL { https://example.com/api/v2 }
|
|
94
|
+
* parseUrl(null, "/api/x"); // URL { http://localhost/api/x }
|
|
95
|
+
* parseUrl(""); // null
|
|
96
|
+
* parseUrl(null); // null
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseUrl(input: UrlLike | null | undefined, ...path: UrlSegmentLike[]): URL | null;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe networking helpers: URL parsing and path joining that
|
|
3
|
+
* gracefully handle partial inputs. No node-only imports, so this
|
|
4
|
+
* module is the canonical home for anything URL-shaped that also has
|
|
5
|
+
* to run in a Vite / Webpack / esbuild client bundle.
|
|
6
|
+
*
|
|
7
|
+
* Public API: {@link joinUrl}, {@link parseUrl}. The
|
|
8
|
+
* single-leading/trailing-slash stripper `stripBoundarySlashes` is
|
|
9
|
+
* private to this module.
|
|
10
|
+
*
|
|
11
|
+
* The server-side `./net.ts` re-exports everything here verbatim and
|
|
12
|
+
* tacks on its own node-only helpers (e.g. `getRandomPort`), so the
|
|
13
|
+
* `netUtils` namespace looks identical from both entry points.
|
|
14
|
+
*/
|
|
15
|
+
// ────────────────────────────────────────────────────────────────
|
|
16
|
+
// URL helpers
|
|
17
|
+
// ────────────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Join URL path segments with `/`, with three pragmatic conveniences
|
|
20
|
+
* for building URLs piecemeal:
|
|
21
|
+
*
|
|
22
|
+
* 1. Nullish or blank segments are dropped, so callers don't have
|
|
23
|
+
* to guard around optional path components.
|
|
24
|
+
* 2. Each string segment has any leading or trailing `/` stripped
|
|
25
|
+
* before joining, so `"/api"` + `"v2/"` round-trips to `/api/v2`
|
|
26
|
+
* regardless of how the caller terminated each part.
|
|
27
|
+
* 3. Array segments recurse, with their joined form inlined into
|
|
28
|
+
* the outer result.
|
|
29
|
+
*
|
|
30
|
+
* The result is prefixed with `/` to make it path-absolute, except
|
|
31
|
+
* when any segment carries an explicit `://` scheme (e.g.
|
|
32
|
+
* `"https://x.com"`), in which case the scheme is preserved verbatim.
|
|
33
|
+
*
|
|
34
|
+
* Returns `""` when every input was nullish or blank.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* joinUrl("a", "b"); // "/a/b"
|
|
38
|
+
* joinUrl("/api/", "/v2/", "x"); // "/api/v2/x"
|
|
39
|
+
* joinUrl(["a", "b"], "c"); // "/a/b/c"
|
|
40
|
+
* joinUrl("https://ex.com", "/api/x"); // "https://ex.com/api/x"
|
|
41
|
+
* joinUrl(null, "", "x", undefined); // "/x"
|
|
42
|
+
* joinUrl(); // ""
|
|
43
|
+
* joinUrl(null); // ""
|
|
44
|
+
*/
|
|
45
|
+
export function joinUrl(...urlSegments) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (const segment of urlSegments) {
|
|
48
|
+
if (segment == null)
|
|
49
|
+
continue;
|
|
50
|
+
if (Array.isArray(segment)) {
|
|
51
|
+
// Recurse, then strip the inner result's leading `/` so the
|
|
52
|
+
// outer path-absolute prepend doesn't double up to `//a/b/c`.
|
|
53
|
+
const inner = joinUrl(...segment);
|
|
54
|
+
if (inner)
|
|
55
|
+
parts.push(stripBoundarySlashes(inner));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// `Array.isArray` narrows away the UrlSegmentArray branch but
|
|
59
|
+
// leaves TS believing `segment` could still be a `string` only,
|
|
60
|
+
// which it now is - the type cast is just to silence a known
|
|
61
|
+
// TS limitation around recursive interface narrowing.
|
|
62
|
+
const trimmed = segment.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
continue;
|
|
65
|
+
parts.push(stripBoundarySlashes(trimmed));
|
|
66
|
+
}
|
|
67
|
+
const joined = parts.filter(Boolean).join("/");
|
|
68
|
+
if (!joined)
|
|
69
|
+
return "";
|
|
70
|
+
return joined.includes("://") ? joined : "/" + joined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Coerce a {@link UrlLike} input into a parsed `URL`, returning `null`
|
|
74
|
+
* for anything that cannot be coerced (null/undefined, empty string,
|
|
75
|
+
* malformed URL, an object whose `url` field doesn't parse).
|
|
76
|
+
*
|
|
77
|
+
* Mirrors WHATWG `URL.parse(...)` semantics: parse on success, `null`
|
|
78
|
+
* on failure - never throws.
|
|
79
|
+
*
|
|
80
|
+
* Bare hostnames are upgraded to `https://` before parsing, so callers
|
|
81
|
+
* can hand in user-provided values like `"workspace.cloud.databricks.com"`
|
|
82
|
+
* without an explicit scheme.
|
|
83
|
+
*
|
|
84
|
+
* Optional trailing `path` arguments are appended via {@link joinUrl}.
|
|
85
|
+
* When `input` is nullish (or just `"/"` / blank) but a `path` is
|
|
86
|
+
* provided, the URL is built against `http://localhost`, which is
|
|
87
|
+
* convenient for tests and for callers that resolve the host later.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* parseUrl("example.com"); // URL { https://example.com/ }
|
|
91
|
+
* parseUrl("http://example.com/path"); // URL { http://example.com/path }
|
|
92
|
+
* parseUrl({ url: "https://api.example" }); // URL { https://api.example/ }
|
|
93
|
+
* parseUrl("example.com", "/api", "v2", "items"); // URL { https://example.com/api/v2/items }
|
|
94
|
+
* parseUrl("example.com", ["api", "v2"]); // URL { https://example.com/api/v2 }
|
|
95
|
+
* parseUrl(null, "/api/x"); // URL { http://localhost/api/x }
|
|
96
|
+
* parseUrl(""); // null
|
|
97
|
+
* parseUrl(null); // null
|
|
98
|
+
*/
|
|
99
|
+
export function parseUrl(input, ...path) {
|
|
100
|
+
if (typeof input === "string") {
|
|
101
|
+
input = input.trim();
|
|
102
|
+
const match = input.match(/^([a-z][a-z0-9+.-]*):\/\/(.*)$/i);
|
|
103
|
+
const rest = match?.[2] ?? input;
|
|
104
|
+
if (!rest || rest === "/")
|
|
105
|
+
return parseUrl(null, ...path);
|
|
106
|
+
const scheme = match?.[1];
|
|
107
|
+
if (!scheme)
|
|
108
|
+
input = `https://${input}`;
|
|
109
|
+
}
|
|
110
|
+
const joinedPath = joinUrl(...path);
|
|
111
|
+
if (input == null) {
|
|
112
|
+
if (joinedPath)
|
|
113
|
+
return parseUrl("http://localhost", joinedPath);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (input instanceof URL) {
|
|
117
|
+
if (joinedPath)
|
|
118
|
+
return parseUrl(input.toString(), joinedPath);
|
|
119
|
+
return input;
|
|
120
|
+
}
|
|
121
|
+
if (typeof input === "string") {
|
|
122
|
+
const candidate = joinUrl(input, joinedPath);
|
|
123
|
+
if (candidate) {
|
|
124
|
+
try {
|
|
125
|
+
return new URL(candidate);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Fall through to `null`.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return parseUrl(input.url, joinedPath);
|
|
134
|
+
}
|
|
135
|
+
// ────────────────────────────────────────────────────────────────
|
|
136
|
+
// Private helpers
|
|
137
|
+
// ────────────────────────────────────────────────────────────────
|
|
138
|
+
/** Strip a single leading `/` and a single trailing `/`, if present. */
|
|
139
|
+
function stripBoundarySlashes(s) {
|
|
140
|
+
let out = s;
|
|
141
|
+
if (out.startsWith("/"))
|
|
142
|
+
out = out.slice(1);
|
|
143
|
+
if (out.endsWith("/"))
|
|
144
|
+
out = out.slice(0, -1);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side networking helpers. Re-exports every browser-safe URL
|
|
3
|
+
* helper from {@link ./net.browser.ts} verbatim and adds node-only
|
|
4
|
+
* additions (currently {@link getRandomPort}), so the `netUtils`
|
|
5
|
+
* namespace exposes the same URL surface from either entry point and
|
|
6
|
+
* picks up node-only helpers automatically on the server.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./net.browser.js";
|
|
9
|
+
/**
|
|
10
|
+
* Bind a transient TCP listener on port `0`, read the OS-assigned
|
|
11
|
+
* port, close the listener, and resolve with the port. Used to grab
|
|
12
|
+
* a free local port for tests, devloops, and child processes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getRandomPort(): Promise<number>;
|
package/dist/src/net.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side networking helpers. Re-exports every browser-safe URL
|
|
3
|
+
* helper from {@link ./net.browser.ts} verbatim and adds node-only
|
|
4
|
+
* additions (currently {@link getRandomPort}), so the `netUtils`
|
|
5
|
+
* namespace exposes the same URL surface from either entry point and
|
|
6
|
+
* picks up node-only helpers automatically on the server.
|
|
7
|
+
*/
|
|
8
|
+
import net from "node:net";
|
|
9
|
+
export * from "./net.browser.js";
|
|
10
|
+
/**
|
|
11
|
+
* Bind a transient TCP listener on port `0`, read the OS-assigned
|
|
12
|
+
* port, close the listener, and resolve with the port. Used to grab
|
|
13
|
+
* a free local port for tests, devloops, and child processes.
|
|
14
|
+
*/
|
|
15
|
+
export async function getRandomPort() {
|
|
16
|
+
return await new Promise((resolve, reject) => {
|
|
17
|
+
const server = net.createServer();
|
|
18
|
+
server.listen(0, () => {
|
|
19
|
+
const address = server.address();
|
|
20
|
+
if (!address || typeof address === "string") {
|
|
21
|
+
reject(new Error("Failed to get port"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const port = address.port;
|
|
25
|
+
server.close(() => resolve(port));
|
|
26
|
+
});
|
|
27
|
+
server.on("error", reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project introspection helpers shared across AppKit plugins.
|
|
3
|
+
*
|
|
4
|
+
* Resolve a human-friendly project name and parse git remote URLs into
|
|
5
|
+
* repo names. Exposed as `projectUtils.*` from the shared barrel so
|
|
6
|
+
* naming inside this module drops the redundant `project` prefix:
|
|
7
|
+
* `projectUtils.name()` instead of `projectName()`, etc.
|
|
8
|
+
*
|
|
9
|
+
* **Server-only.** Imports `node:fs`, `node:path`, `node:child_process`,
|
|
10
|
+
* and `node:util` at module load. Browser bundles must use
|
|
11
|
+
* `@dbx-tools/shared`'s `index.client.ts` entry point, which
|
|
12
|
+
* skips this module entirely.
|
|
13
|
+
*/
|
|
14
|
+
export interface NameOptions {
|
|
15
|
+
/** Directory to start from. Defaults to `process.cwd()`. */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a human-friendly project name for the repo rooted at `cwd`.
|
|
20
|
+
*
|
|
21
|
+
* Order:
|
|
22
|
+
* 1. `name` from the root `package.json` (via `npm pkg get name` when available,
|
|
23
|
+
* otherwise read the file after locating the root).
|
|
24
|
+
* 2. Repository name from `git remote get-url origin`.
|
|
25
|
+
* 3. Basename of the project root directory.
|
|
26
|
+
*/
|
|
27
|
+
export declare function name(options?: NameOptions): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Parse a git remote URL (`https://...`, `git@host:owner/repo.git`, etc.)
|
|
30
|
+
* and return the repo segment, stripping any `.git` suffix. Returns
|
|
31
|
+
* `undefined` for empty or unparsable input.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseGitRemote(url: string): string | undefined;
|