@tnldotdev/tnl 0.1.0-rc.29 → 0.1.0-rc.30

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/dist/bin/tnl.js CHANGED
@@ -9,7 +9,26 @@ try {
9
9
  process.execve(binary, [binary, ...process.argv.slice(2)], process.env);
10
10
  }
11
11
  catch (error) {
12
- const message = error instanceof Error ? error.message : String(error);
12
+ let message = "launcher failed";
13
+ try {
14
+ message = error instanceof Error ? error.message : String(error);
15
+ }
16
+ catch { }
17
+ message = [...message]
18
+ .map((character) => {
19
+ const codePoint = character.codePointAt(0);
20
+ return codePoint !== undefined &&
21
+ (codePoint <= 0x1f ||
22
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
23
+ codePoint === 0x2028 ||
24
+ codePoint === 0x2029)
25
+ ? " "
26
+ : character;
27
+ })
28
+ .join("")
29
+ .trim();
30
+ if (message === "")
31
+ message = "launcher failed";
13
32
  process.stderr.write(`tnl: ${message}\n`);
14
33
  process.exitCode = 1;
15
34
  }
@@ -12,11 +12,11 @@ export interface TnlConfig {
12
12
  */
13
13
  tunnel?: {
14
14
  /**
15
- * Complete route hostname to publish.
15
+ * Complete public URL hostname to publish.
16
16
  */
17
17
  host?: string;
18
18
  /**
19
- * One DNS label beneath the current member namespace.
19
+ * One DNS label beneath the current namespace.
20
20
  */
21
21
  subdomain?: string;
22
22
  /**
@@ -58,15 +58,15 @@ export interface TnlConfig {
58
58
  */
59
59
  team?: string;
60
60
  /**
61
- * Route and tunnel overrides for this service.
61
+ * PublicURL and tunnel overrides for this service.
62
62
  */
63
63
  tunnel?: {
64
64
  /**
65
- * Complete route hostname to publish.
65
+ * Complete public URL hostname to publish.
66
66
  */
67
67
  host?: string;
68
68
  /**
69
- * One DNS label beneath the current member namespace.
69
+ * One DNS label beneath the current namespace.
70
70
  */
71
71
  subdomain?: string;
72
72
  /**
@@ -0,0 +1,3 @@
1
+ import type { TnlRuntime } from "./index.js";
2
+ /** Browser-safe project metadata; listener registration is server-only. */
3
+ export declare const tnl: TnlRuntime;
@@ -0,0 +1,11 @@
1
+ import { parseRuntimePayload } from "./internal/runtime.js";
2
+ const runtime = parseRuntimePayload(typeof process === "undefined" ? undefined : process.env?.TNL_PROJECT_RUNTIME);
3
+ /** Browser-safe project metadata; listener registration is server-only. */
4
+ export const tnl = Object.freeze({
5
+ ...runtime,
6
+ port: 3000,
7
+ dev: runtime?.dev ?? false,
8
+ async register() {
9
+ throw new Error("tnl.register is available only on a development server");
10
+ },
11
+ });
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { type ProjectMetadata } from "./internal/runtime.js";
2
+ import type { LocalHTTPServer } from "./internal/register.js";
2
3
  /** Extended with service types from the generated `.tnl/project.d.ts` file. */
3
4
  export interface TnlProjectMetadata {
4
5
  }
5
6
  type RegisteredTnlProject = keyof TnlProjectMetadata extends never ? ProjectMetadata : Readonly<TnlProjectMetadata>;
6
- type TnlProject = RegisteredTnlProject & {
7
- readonly runningUnderTnlDev: boolean;
8
- };
9
- /** Project metadata added by a tnl framework integration during development. */
10
- export declare const tnl: TnlProject | undefined;
7
+ export type TnlRuntime = Readonly<Partial<RegisteredTnlProject> & {
8
+ readonly port: number;
9
+ readonly dev: boolean;
10
+ register(server: LocalHTTPServer): Promise<void>;
11
+ }>;
12
+ export declare const tnl: TnlRuntime;
11
13
  export {};
package/dist/index.js CHANGED
@@ -1,6 +1,19 @@
1
+ import { developmentPort } from "./internal/port.js";
1
2
  import { parseRuntimePayload } from "./internal/runtime.js";
2
3
  const serializedRuntime = typeof process === "undefined" ? undefined : process.env?.TNL_PROJECT_RUNTIME;
3
- /** Project metadata added by a tnl framework integration during development. */
4
+ /** Server port and optional browser-safe project metadata for this process. */
4
5
  // The parser validates the runtime shape; generated project declarations supply
5
6
  // project-specific service names and literals unavailable to this shared module.
6
- export const tnl = parseRuntimePayload(serializedRuntime);
7
+ const runtime = parseRuntimePayload(serializedRuntime);
8
+ export const tnl = Object.freeze({
9
+ ...runtime,
10
+ port: developmentPort(typeof process === "undefined" ? {} : (process.env ?? {}), "Bun" in globalThis),
11
+ dev: runtime?.dev ?? false,
12
+ async register(server) {
13
+ if (typeof process === "undefined") {
14
+ throw new Error("tnl.register is available only on a development server");
15
+ }
16
+ const { registerServer } = await import("./internal/register.js");
17
+ await registerServer(server);
18
+ },
19
+ });
@@ -6,7 +6,7 @@ export interface TnlDevBootstrap {
6
6
  }
7
7
  export interface ProjectDocument extends ProjectMetadata {
8
8
  readonly projectRoot: string;
9
- readonly runningUnderTnlDev: boolean;
9
+ readonly dev: boolean;
10
10
  readonly serviceDirectories: Readonly<Record<string, string>>;
11
11
  readonly version: 1;
12
12
  }
@@ -23,7 +23,7 @@ export interface DevelopmentContext {
23
23
  export interface TnlTunnelAssignment extends TnlDevBootstrap {
24
24
  readonly framework: string;
25
25
  readonly hostname: string;
26
- readonly memberNamespace: string;
26
+ readonly namespace: string;
27
27
  readonly project: ProjectRuntime;
28
28
  readonly publicURL: `https://${string}`;
29
29
  readonly service: string | null;
@@ -34,7 +34,7 @@ export declare function readDevelopmentContext(environment?: TnlDevEnvironment,
34
34
  export declare function requestTunnelAssignment(framework: string, bootstrap: TnlDevBootstrap): Promise<TnlTunnelAssignment>;
35
35
  export declare function canonicalLoopbackTarget(host: string, port: number): CanonicalLoopbackTarget;
36
36
  export declare function registerLocalTarget(assignment: TnlTunnelAssignment, target: CanonicalLoopbackTarget): Promise<void>;
37
- export declare function runtimePayload(project: ProjectMetadata, runningUnderTnlDev: boolean): string;
37
+ export declare function runtimePayload(project: ProjectMetadata, dev: boolean): string;
38
38
  export declare function discoverProject(cwd: string): ProjectDiscovery | null;
39
39
  export declare function socketIdentity(projectRoot: string, service: string | null): string;
40
40
  /** Parses a decimal listener port from 1 through 65535. */
@@ -38,7 +38,7 @@ export async function requestTunnelAssignment(framework, bootstrap) {
38
38
  throw new Error("tnl framework name is invalid");
39
39
  }
40
40
  const body = JSON.stringify({ protocol: 1, framework });
41
- const response = await sendRequest(bootstrap, framework, "/v1/configure", body, 200);
41
+ const response = await sendRequest(bootstrap, framework, "/v1/configure", body, 200, "configuration");
42
42
  return parseAssignment(response, bootstrap, framework);
43
43
  }
44
44
  export function canonicalLoopbackTarget(host, port) {
@@ -63,10 +63,10 @@ export function canonicalLoopbackTarget(host, port) {
63
63
  }
64
64
  export async function registerLocalTarget(assignment, target) {
65
65
  const body = JSON.stringify({ protocol: 1, framework: assignment.framework, target });
66
- await sendRequest(assignment, assignment.framework, "/v1/target", body, 204);
66
+ await sendRequest(assignment, assignment.framework, "/v1/target", body, 204, "target registration");
67
67
  }
68
- export function runtimePayload(project, runningUnderTnlDev) {
69
- return serializeRuntimePayload(project, runningUnderTnlDev);
68
+ export function runtimePayload(project, dev) {
69
+ return serializeRuntimePayload(project, dev);
70
70
  }
71
71
  export function discoverProject(cwd) {
72
72
  let directory = absoluteNormalizedPath(cwd, "working directory");
@@ -147,7 +147,7 @@ function discoverDevSocket(discovery, environment) {
147
147
  }
148
148
  function parseAssignment(value, bootstrap, framework) {
149
149
  const object = record(value, "tnl dev response");
150
- exactKeys(object, ["hostname", "memberNamespace", "project", "protocol", "publicURL", "service", "tunnelID"], "tnl dev response");
150
+ exactKeys(object, ["hostname", "namespace", "project", "protocol", "publicURL", "service", "tunnelID"], "tnl dev response");
151
151
  if (object.protocol !== 1) {
152
152
  throw new Error("tnl dev returned an inconsistent tunnel assignment");
153
153
  }
@@ -157,20 +157,18 @@ function parseAssignment(value, bootstrap, framework) {
157
157
  if (object.service !== null && !validServiceName(object.service)) {
158
158
  throw new Error("tnl dev returned an invalid service");
159
159
  }
160
- const memberNamespace = requiredHostname(object.memberNamespace, "tnl dev returned member namespace");
160
+ const responseNamespace = requiredHostname(object.namespace, "tnl dev returned namespace");
161
161
  const hostname = requiredHostname(object.hostname, "tnl dev returned public hostname");
162
162
  const publicURL = `https://${hostname}`;
163
163
  if (object.publicURL !== publicURL) {
164
164
  throw new Error("tnl dev returned an invalid public URL");
165
165
  }
166
166
  const project = parseProjectRuntime(object.project, "tnl dev project metadata");
167
- if (!project.runningUnderTnlDev) {
167
+ if (!project.dev) {
168
168
  throw new Error("tnl dev returned project metadata outside tnl dev");
169
169
  }
170
- const assignmentNamespace = object.service === null
171
- ? project.memberNamespace
172
- : project.services[object.service]?.memberNamespace;
173
- if (assignmentNamespace !== memberNamespace) {
170
+ const projectNamespace = object.service === null ? project.namespace : project.services[object.service]?.namespace;
171
+ if (projectNamespace !== responseNamespace) {
174
172
  throw new Error("tnl dev returned inconsistent project metadata");
175
173
  }
176
174
  if (object.service !== null && project.services[object.service] === undefined) {
@@ -183,7 +181,7 @@ function parseAssignment(value, bootstrap, framework) {
183
181
  ...bootstrap,
184
182
  framework,
185
183
  hostname,
186
- memberNamespace,
184
+ namespace: responseNamespace,
187
185
  project,
188
186
  publicURL,
189
187
  service: object.service,
@@ -202,17 +200,17 @@ function parseProjectDocument(serialized, description, projectRoot) {
202
200
  }
203
201
  function parseProjectDocumentValue(value, description, projectRoot) {
204
202
  const object = record(value, description);
205
- exactKeys(object, ["memberNamespace", "runningUnderTnlDev", "serviceDirectories", "services", "version"], description);
203
+ exactKeys(object, ["dev", "namespace", "serviceDirectories", "services", "version"], description);
206
204
  if (object.version !== 1) {
207
205
  throw new Error(`${description} has an unsupported version`);
208
206
  }
209
- if (typeof object.runningUnderTnlDev !== "boolean") {
210
- throw new Error(`${description} has an invalid runningUnderTnlDev value`);
207
+ if (typeof object.dev !== "boolean") {
208
+ throw new Error(`${description} has an invalid dev value`);
211
209
  }
212
- if (object.runningUnderTnlDev) {
210
+ if (object.dev) {
213
211
  throw new Error(`${description} cannot be marked as running under tnl dev`);
214
212
  }
215
- const project = parseProjectMetadata({ memberNamespace: object.memberNamespace, services: object.services }, description);
213
+ const project = parseProjectMetadata({ namespace: object.namespace, services: object.services }, description);
216
214
  const directoryValues = record(object.serviceDirectories, `${description} service directories`);
217
215
  const names = Object.keys(project.services);
218
216
  if (Object.keys(directoryValues).length !== names.length ||
@@ -226,13 +224,13 @@ function parseProjectDocumentValue(value, description, projectRoot) {
226
224
  return Object.freeze({
227
225
  ...project,
228
226
  projectRoot,
229
- runningUnderTnlDev: object.runningUnderTnlDev,
227
+ dev: object.dev,
230
228
  serviceDirectories: Object.freeze(serviceDirectories),
231
229
  version: 1,
232
230
  });
233
231
  }
234
232
  function projectMetadata(document) {
235
- return Object.freeze({ memberNamespace: document.memberNamespace, services: document.services });
233
+ return Object.freeze({ namespace: document.namespace, services: document.services });
236
234
  }
237
235
  function selectService(document, cwd) {
238
236
  const absoluteCwd = absoluteNormalizedPath(cwd, "working directory");
@@ -262,64 +260,105 @@ function relativeDirectory(value, description) {
262
260
  }
263
261
  return value;
264
262
  }
265
- function sendRequest(bootstrap, framework, requestPath, body, expectedStatus) {
263
+ function sendRequest(bootstrap, framework, requestPath, body, expectedStatus, operation) {
266
264
  return new Promise((resolve, reject) => {
267
- const request = http.request({
268
- socketPath: bootstrap.socket,
269
- path: requestPath,
270
- method: "POST",
271
- headers: {
272
- "Content-Length": Buffer.byteLength(body),
273
- "Content-Type": "application/json",
274
- },
275
- }, (response) => {
276
- response.on("error", reject);
277
- const declaredLength = Number(response.headers["content-length"]);
278
- if (Number.isFinite(declaredLength) && declaredLength > maximumResponseBytes) {
279
- response.destroy(new Error("tnl dev returned an oversized response"));
265
+ let settled = false;
266
+ const succeed = (value) => {
267
+ if (settled)
280
268
  return;
281
- }
282
- const chunks = [];
283
- let size = 0;
284
- response.on("data", (chunk) => {
285
- size += chunk.length;
286
- if (size > maximumResponseBytes) {
287
- response.destroy(new Error("tnl dev returned an oversized response"));
288
- return;
289
- }
290
- chunks.push(chunk);
291
- });
292
- response.on("end", () => {
293
- const data = Buffer.concat(chunks).toString("utf8").trim();
294
- if (response.statusCode !== expectedStatus) {
295
- reject(new Error(`tnl dev rejected the ${framework} request with status ${response.statusCode}${data ? `: ${data}` : ""}`));
269
+ settled = true;
270
+ resolve(value);
271
+ };
272
+ const fail = (error) => {
273
+ if (settled)
274
+ return;
275
+ settled = true;
276
+ reject(error);
277
+ };
278
+ const failTransport = (phase, cause) => {
279
+ fail(new Error(`tnl dev ${operation} ${phase} failed`, { cause }));
280
+ };
281
+ let request;
282
+ try {
283
+ request = http.request({
284
+ socketPath: bootstrap.socket,
285
+ path: requestPath,
286
+ method: "POST",
287
+ headers: {
288
+ "Content-Length": Buffer.byteLength(body),
289
+ "Content-Type": "application/json",
290
+ },
291
+ }, (response) => {
292
+ response.on("error", (error) => failTransport("response", error));
293
+ response.on("close", () => {
294
+ if (!response.complete) {
295
+ failTransport("response", new Error("tnl dev response closed before it was complete"));
296
+ }
297
+ });
298
+ const declaredLength = Number(response.headers["content-length"]);
299
+ if (Number.isFinite(declaredLength) && declaredLength > maximumResponseBytes) {
300
+ fail(new Error("tnl dev returned an oversized response"));
301
+ response.destroy();
296
302
  return;
297
303
  }
298
- if (expectedStatus === 204) {
299
- if (data !== "") {
300
- reject(new Error("tnl dev returned an unexpected target response"));
304
+ const chunks = [];
305
+ let size = 0;
306
+ response.on("data", (chunk) => {
307
+ if (settled)
308
+ return;
309
+ size += chunk.length;
310
+ if (size > maximumResponseBytes) {
311
+ fail(new Error("tnl dev returned an oversized response"));
312
+ response.destroy();
301
313
  return;
302
314
  }
303
- resolve(undefined);
304
- return;
305
- }
306
- if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json") {
307
- reject(new Error("tnl dev returned a non-JSON configuration"));
308
- return;
309
- }
310
- try {
311
- resolve(JSON.parse(data));
312
- }
313
- catch (error) {
314
- reject(new Error("tnl dev returned an invalid configuration", { cause: error }));
315
- }
315
+ chunks.push(chunk);
316
+ });
317
+ response.on("end", () => {
318
+ if (!response.complete) {
319
+ failTransport("response", new Error("tnl dev response ended before it was complete"));
320
+ return;
321
+ }
322
+ const data = Buffer.concat(chunks).toString("utf8").trim();
323
+ if (response.statusCode !== expectedStatus) {
324
+ fail(new Error(`tnl dev rejected the ${framework} request with status ${response.statusCode}${data ? `: ${data}` : ""}`));
325
+ return;
326
+ }
327
+ if (expectedStatus === 204) {
328
+ if (data !== "") {
329
+ fail(new Error("tnl dev returned an unexpected target response"));
330
+ return;
331
+ }
332
+ succeed(undefined);
333
+ return;
334
+ }
335
+ if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json") {
336
+ fail(new Error("tnl dev returned a non-JSON configuration"));
337
+ return;
338
+ }
339
+ try {
340
+ succeed(JSON.parse(data));
341
+ }
342
+ catch (error) {
343
+ fail(new Error("tnl dev returned an invalid configuration", { cause: error }));
344
+ }
345
+ });
316
346
  });
317
- });
347
+ }
348
+ catch (error) {
349
+ failTransport("request", error);
350
+ return;
351
+ }
318
352
  request.setTimeout(registrationTimeoutMilliseconds, () => {
319
353
  request.destroy(new Error("timed out configuring the target with tnl dev"));
320
354
  });
321
- request.on("error", reject);
322
- request.end(body);
355
+ request.on("error", (error) => failTransport("request", error));
356
+ try {
357
+ request.end(body);
358
+ }
359
+ catch (error) {
360
+ failTransport("request", error);
361
+ }
323
362
  });
324
363
  }
325
364
  function readBoundedFile(file) {
@@ -5,6 +5,7 @@ import process from "node:process";
5
5
  import { nativeTargets } from "./native-targets.js";
6
6
  const require = createRequire(import.meta.url);
7
7
  const launcherManifest = new URL("../../package.json", import.meta.url);
8
+ const packageVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
8
9
  const nativePackages = new Map(nativeTargets.map(({ platform, architecture, packageName }) => [
9
10
  `${platform}-${architecture}`,
10
11
  packageName,
@@ -29,10 +30,10 @@ export function resolveNativeBinary(options = {}) {
29
30
  }
30
31
  throw error;
31
32
  }
32
- const mainManifest = readManifest(launcherManifest);
33
- const nativeManifest = readManifest(nativeManifestPath);
33
+ const mainManifest = readManifest(launcherManifest, "@tnldotdev/tnl");
34
+ const nativeManifest = readManifest(nativeManifestPath, packageName);
34
35
  if (nativeManifest.version !== mainManifest.version) {
35
- throw new Error(`${packageName}@${String(nativeManifest.version)} does not match @tnldotdev/tnl@${String(mainManifest.version)}`);
36
+ throw new Error(`${packageName}@${nativeManifest.version} does not match @tnldotdev/tnl@${mainManifest.version}`);
36
37
  }
37
38
  const binary = path.join(path.dirname(nativeManifestPath), "bin", "tnl");
38
39
  try {
@@ -43,13 +44,37 @@ export function resolveNativeBinary(options = {}) {
43
44
  }
44
45
  return binary;
45
46
  }
46
- function readManifest(file) {
47
- const value = JSON.parse(readFileSync(file, "utf8"));
47
+ function readManifest(file, packageName) {
48
+ let serialized;
49
+ try {
50
+ serialized = readFileSync(file, "utf8");
51
+ }
52
+ catch (error) {
53
+ throw new Error(`failed to read ${packageName} package manifest`, { cause: error });
54
+ }
55
+ let value;
56
+ try {
57
+ value = JSON.parse(serialized);
58
+ }
59
+ catch (error) {
60
+ throw new Error(`${packageName} package manifest is not valid JSON`, { cause: error });
61
+ }
62
+ try {
63
+ return parseManifest(value);
64
+ }
65
+ catch (error) {
66
+ throw new Error(`${packageName} package manifest has an invalid shape`, { cause: error });
67
+ }
68
+ }
69
+ function parseManifest(value) {
48
70
  if (value === null ||
49
71
  typeof value !== "object" ||
72
+ Array.isArray(value) ||
50
73
  !("version" in value) ||
51
- typeof value.version !== "string") {
52
- throw new Error("package manifest has an invalid shape");
74
+ typeof value.version !== "string" ||
75
+ value.version.length > 256 ||
76
+ !packageVersionPattern.test(value.version)) {
77
+ throw new TypeError("package manifest version must be a valid semantic version");
53
78
  }
54
79
  return { version: value.version };
55
80
  }
@@ -0,0 +1,2 @@
1
+ /** Selects a free port under tnl dev, or the runtime's usual local port. */
2
+ export declare function developmentPort(environment: Record<string, string | undefined>, bun: boolean): number;
@@ -0,0 +1,21 @@
1
+ /** Selects a free port under tnl dev, or the runtime's usual local port. */
2
+ export function developmentPort(environment, bun) {
3
+ if (environment.TNL_DEV_PROTOCOL !== undefined) {
4
+ if (environment.TNL_DEV_PROTOCOL !== "1") {
5
+ throw new Error("unsupported tnl dev protocol; upgrade tnl and its integrations");
6
+ }
7
+ const forced = environment.TNL_DEV_PORT;
8
+ return forced === undefined ? 0 : parsePort(forced, "TNL_DEV_PORT");
9
+ }
10
+ const configured = bun
11
+ ? (environment.BUN_PORT ?? environment.PORT ?? environment.NODE_PORT)
12
+ : environment.PORT;
13
+ return configured === undefined ? 3000 : parsePort(configured, "development port");
14
+ }
15
+ function parsePort(value, name) {
16
+ const port = Number(value);
17
+ if (!Number.isInteger(port) || port < 1 || port > 65535 || String(port) !== value) {
18
+ throw new Error(`${name} must be a port between 1 and 65535`);
19
+ }
20
+ return port;
21
+ }
@@ -0,0 +1,20 @@
1
+ export interface BunHTTPServer {
2
+ readonly hostname?: string | undefined;
3
+ readonly port?: number | undefined;
4
+ readonly url: URL;
5
+ stop(closeActiveConnections?: boolean): Promise<void> | void;
6
+ }
7
+ /** Small structural surface shared by Node HTTP servers, without Node types in browser declarations. */
8
+ export interface NodeHTTPServer {
9
+ readonly listening: boolean;
10
+ address(): unknown;
11
+ once(event: "listening", listener: () => void): unknown;
12
+ once(event: "error", listener: (error: Error) => void): unknown;
13
+ off(event: "listening", listener: () => void): unknown;
14
+ off(event: "error", listener: (error: Error) => void): unknown;
15
+ close(callback?: (error?: Error) => void): unknown;
16
+ closeAllConnections?(): void;
17
+ }
18
+ export type LocalHTTPServer = NodeHTTPServer | BunHTTPServer;
19
+ /** Registers a bound Node or Bun HTTP listener with this tnl dev invocation. */
20
+ export declare function registerServer(server: LocalHTTPServer): Promise<void>;
@@ -0,0 +1,79 @@
1
+ import { Server as HTTPSServer } from "node:https";
2
+ import { canonicalLoopbackTarget, readDevelopmentContext, registerLocalTarget, requestTunnelAssignment, } from "./dev.js";
3
+ /** Registers a bound Node or Bun HTTP listener with this tnl dev invocation. */
4
+ export async function registerServer(server) {
5
+ const { bootstrap } = readDevelopmentContext();
6
+ if (bootstrap === null)
7
+ return;
8
+ try {
9
+ const target = await listeningTarget(server);
10
+ const framework = isBunServer(server) ? "bun" : "node";
11
+ const assignment = await requestTunnelAssignment(framework, bootstrap);
12
+ await registerLocalTarget(assignment, target);
13
+ }
14
+ catch (error) {
15
+ try {
16
+ await closeServer(server);
17
+ }
18
+ catch (closeError) {
19
+ throw new AggregateError([error, closeError], "tnl target registration and server cleanup failed", {
20
+ cause: error,
21
+ });
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ async function listeningTarget(server) {
27
+ if (isBunServer(server)) {
28
+ const hostname = server.hostname ?? server.url.hostname;
29
+ if (server.url.protocol !== "http:" ||
30
+ typeof server.port !== "number" ||
31
+ !Number.isInteger(server.port) ||
32
+ typeof hostname !== "string") {
33
+ throw new Error("Bun did not report a TCP listening address");
34
+ }
35
+ return canonicalLoopbackTarget(hostname, server.port);
36
+ }
37
+ if (server instanceof HTTPSServer) {
38
+ throw new Error("tnl dev requires a local HTTP listener");
39
+ }
40
+ if (server.address() === null) {
41
+ await new Promise((resolve, reject) => {
42
+ const onListening = () => {
43
+ server.off("error", onError);
44
+ resolve();
45
+ };
46
+ const onError = (error) => {
47
+ server.off("listening", onListening);
48
+ reject(error);
49
+ };
50
+ server.once("listening", onListening);
51
+ server.once("error", onError);
52
+ });
53
+ }
54
+ const address = server.address();
55
+ if (address === null ||
56
+ typeof address !== "object" ||
57
+ !("address" in address) ||
58
+ !("port" in address) ||
59
+ typeof address.address !== "string" ||
60
+ typeof address.port !== "number") {
61
+ throw new Error("Node did not report a TCP listening address");
62
+ }
63
+ return canonicalLoopbackTarget(address.address, address.port);
64
+ }
65
+ async function closeServer(server) {
66
+ if (isBunServer(server)) {
67
+ await server.stop(true);
68
+ return;
69
+ }
70
+ if (server.listening) {
71
+ await new Promise((resolve, reject) => {
72
+ server.close((error) => (error ? reject(error) : resolve()));
73
+ server.closeAllConnections?.();
74
+ });
75
+ }
76
+ }
77
+ function isBunServer(server) {
78
+ return "stop" in server && typeof server.stop === "function";
79
+ }
@@ -1,14 +1,14 @@
1
1
  export interface ProjectServiceMetadata {
2
- readonly memberNamespace: string;
2
+ readonly namespace: string;
3
3
  readonly hostname: string;
4
4
  readonly url: `https://${string}`;
5
5
  }
6
6
  export interface ProjectMetadata {
7
- readonly memberNamespace: string;
7
+ readonly namespace: string;
8
8
  readonly services: Readonly<Record<string, ProjectServiceMetadata | undefined>>;
9
9
  }
10
10
  export interface ProjectRuntime extends ProjectMetadata {
11
- readonly runningUnderTnlDev: boolean;
11
+ readonly dev: boolean;
12
12
  }
13
13
  /** Checks browser-safe project metadata and makes it read-only. */
14
14
  export declare function parseProjectMetadata(value: unknown, description: string): ProjectMetadata;
@@ -16,7 +16,7 @@ export declare function parseProjectMetadata(value: unknown, description: string
16
16
  export declare function validServiceName(value: unknown): value is string;
17
17
  export declare function parseRuntimePayload(serialized: string | undefined): ProjectRuntime | undefined;
18
18
  export declare function parseProjectRuntime(value: unknown, description: string): ProjectRuntime;
19
- export declare function serializeRuntimePayload(project: ProjectMetadata, runningUnderTnlDev: boolean): string;
19
+ export declare function serializeRuntimePayload(project: ProjectMetadata, dev: boolean): string;
20
20
  export declare function requiredHostname(value: unknown, description: string): string;
21
21
  export declare function exactKeys(object: Record<string, unknown>, expected: readonly string[], description: string): void;
22
22
  export declare function record(value: unknown, description: string): Record<string, unknown>;
@@ -4,8 +4,8 @@ const serviceNamePattern = /^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
4
4
  /** Checks browser-safe project metadata and makes it read-only. */
5
5
  export function parseProjectMetadata(value, description) {
6
6
  const object = record(value, description);
7
- exactKeys(object, ["memberNamespace", "services"], description);
8
- const memberNamespace = requiredHostname(object.memberNamespace, `${description} member namespace`);
7
+ exactKeys(object, ["namespace", "services"], description);
8
+ const projectNamespace = requiredHostname(object.namespace, `${description} namespace`);
9
9
  const servicesObject = record(object.services, `${description} services`);
10
10
  const entries = Object.entries(servicesObject);
11
11
  if (entries.length > maximumServices) {
@@ -18,8 +18,8 @@ export function parseProjectMetadata(value, description) {
18
18
  throw new Error(`${description} contains an invalid service name ${JSON.stringify(name)}`);
19
19
  }
20
20
  const service = record(value, `${description} service ${JSON.stringify(name)}`);
21
- exactKeys(service, ["hostname", "memberNamespace", "url"], `${description} service ${JSON.stringify(name)}`);
22
- const serviceMemberNamespace = requiredHostname(service.memberNamespace, `${description} service ${JSON.stringify(name)} member namespace`);
21
+ exactKeys(service, ["hostname", "namespace", "url"], `${description} service ${JSON.stringify(name)}`);
22
+ const serviceNamespace = requiredHostname(service.namespace, `${description} service ${JSON.stringify(name)} namespace`);
23
23
  const hostname = requiredHostname(service.hostname, `${description} service ${JSON.stringify(name)} hostname`);
24
24
  const url = `https://${hostname}`;
25
25
  if (service.url !== url) {
@@ -30,13 +30,13 @@ export function parseProjectMetadata(value, description) {
30
30
  }
31
31
  hostnames.add(hostname);
32
32
  services[name] = Object.freeze({
33
- memberNamespace: serviceMemberNamespace,
33
+ namespace: serviceNamespace,
34
34
  hostname,
35
35
  url,
36
36
  });
37
37
  }
38
38
  return Object.freeze({
39
- memberNamespace,
39
+ namespace: projectNamespace,
40
40
  services: Object.freeze(services),
41
41
  });
42
42
  }
@@ -62,20 +62,20 @@ export function parseRuntimePayload(serialized) {
62
62
  }
63
63
  export function parseProjectRuntime(value, description) {
64
64
  const object = record(value, description);
65
- exactKeys(object, ["memberNamespace", "runningUnderTnlDev", "services"], description);
66
- if (typeof object.runningUnderTnlDev !== "boolean") {
67
- throw new Error(`${description} has an invalid runningUnderTnlDev value`);
65
+ exactKeys(object, ["dev", "namespace", "services"], description);
66
+ if (typeof object.dev !== "boolean") {
67
+ throw new Error(`${description} has an invalid dev value`);
68
68
  }
69
- const project = parseProjectMetadata({ memberNamespace: object.memberNamespace, services: object.services }, description);
69
+ const project = parseProjectMetadata({ namespace: object.namespace, services: object.services }, description);
70
70
  return Object.freeze({
71
71
  ...project,
72
- runningUnderTnlDev: object.runningUnderTnlDev,
72
+ dev: object.dev,
73
73
  });
74
74
  }
75
- export function serializeRuntimePayload(project, runningUnderTnlDev) {
75
+ export function serializeRuntimePayload(project, dev) {
76
76
  const serialized = JSON.stringify({
77
- memberNamespace: project.memberNamespace,
78
- runningUnderTnlDev,
77
+ namespace: project.namespace,
78
+ dev,
79
79
  services: project.services,
80
80
  });
81
81
  if (byteLength(serialized) > maximumRuntimeBytes) {
package/dist/vite.js CHANGED
@@ -56,28 +56,41 @@ export default function tnl(...arguments_) {
56
56
  }
57
57
  const originalListen = server.listen.bind(server);
58
58
  server.listen = async (port, isRestart) => {
59
- const listening = await originalListen(port, isRestart);
59
+ let listening;
60
+ try {
61
+ listening = await originalListen(port, isRestart);
62
+ }
63
+ catch (error) {
64
+ return await closeAfterFailure(server.close.bind(server), error);
65
+ }
60
66
  if (localPortRegistered) {
61
67
  return listening;
62
68
  }
63
- const address = server.httpServer?.address();
64
- if (address === null || address === undefined || typeof address === "string") {
65
- await server.close();
66
- throw new Error("Vite did not report its listening port to tnl dev");
67
- }
68
69
  try {
70
+ const address = server.httpServer?.address();
71
+ if (address === null || address === undefined || typeof address === "string") {
72
+ throw new Error("Vite did not report its listening port to tnl dev");
73
+ }
69
74
  await registerLocalTarget(configured, canonicalLoopbackTarget(address.address, address.port));
70
75
  localPortRegistered = true;
71
76
  }
72
77
  catch (error) {
73
- await server.close();
74
- throw error;
78
+ return await closeAfterFailure(server.close.bind(server), error);
75
79
  }
76
80
  return listening;
77
81
  };
78
82
  },
79
83
  };
80
84
  }
85
+ async function closeAfterFailure(close, error) {
86
+ try {
87
+ await close();
88
+ }
89
+ catch (closeError) {
90
+ throw new AggregateError([error, closeError], "Vite listener setup failed and server cleanup also failed", { cause: error });
91
+ }
92
+ throw error;
93
+ }
81
94
  function runtimeDefine(payload) {
82
95
  return {
83
96
  define: {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@tnldotdev/tnl",
3
- "version": "0.1.0-rc.29",
3
+ "version": "0.1.0-rc.30",
4
4
  "bin": {
5
5
  "tnl": "dist/bin/tnl.js"
6
6
  },
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
+ "browser": "./dist/index.browser.js",
10
11
  "import": "./dist/index.js",
11
12
  "default": "./dist/index.js"
12
13
  },
@@ -31,7 +32,21 @@
31
32
  "clean": "node ../../scripts/clean-package.ts",
32
33
  "prepack": "pnpm run build"
33
34
  },
34
- "description": "The tnl client and framework integrations for project-local development.",
35
+ "description": "public urls for every worktree.",
36
+ "keywords": [
37
+ "git-worktree",
38
+ "local-development",
39
+ "localhost",
40
+ "nextjs",
41
+ "ngrok-alternative",
42
+ "self-hosted",
43
+ "tunnel",
44
+ "vite"
45
+ ],
46
+ "homepage": "https://tnl.dev",
47
+ "bugs": {
48
+ "url": "https://github.com/tnldotdev/tnl/issues"
49
+ },
35
50
  "license": "MIT",
36
51
  "repository": {
37
52
  "type": "git",
@@ -49,6 +64,9 @@
49
64
  "access": "public"
50
65
  },
51
66
  "devDependencies": {
67
+ "@hono/node-server": "2.1.1",
68
+ "@types/bun": "1.4.2",
69
+ "hono": "4.13.9",
52
70
  "next": "16.3.5",
53
71
  "react": "19.3.0",
54
72
  "react-dom": "19.3.0",
@@ -70,12 +88,12 @@
70
88
  "node": ">=22.18"
71
89
  },
72
90
  "optionalDependencies": {
73
- "@tnldotdev/tnl-darwin-arm64": "0.1.0-rc.29",
74
- "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.29",
75
- "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.29",
76
- "@tnldotdev/tnl-linux-x64": "0.1.0-rc.29"
91
+ "@tnldotdev/tnl-darwin-arm64": "0.1.0-rc.30",
92
+ "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.30",
93
+ "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.30",
94
+ "@tnldotdev/tnl-linux-x64": "0.1.0-rc.30"
77
95
  },
78
96
  "tnl": {
79
- "commit": "a0c2f155ee7eb907273bfad43e5a691670a058fc"
97
+ "commit": "98fef8d1aa8087f43c3509afb7dac13e84310df6"
80
98
  }
81
99
  }