@tnldotdev/tnl 0.1.0-rc.25 → 0.1.0-rc.27

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { resolveNativeBinary } from "../internal/launcher.js";
4
+ try {
5
+ const binary = resolveNativeBinary();
6
+ if (process.execve === undefined) {
7
+ throw new Error("Node.js does not support replacing the launcher process");
8
+ }
9
+ process.execve(binary, [binary, ...process.argv.slice(2)], process.env);
10
+ }
11
+ catch (error) {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ process.stderr.write(`tnl: ${message}\n`);
14
+ process.exitCode = 1;
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { TnlConfigInput } from "./config.gen.js";
2
+ export type * from "./config.gen.js";
3
+ /** Adds type checking to a version-1 tnl.config.ts configuration. */
4
+ export declare function defineConfig<const Config extends TnlConfigInput>(config: Config): Config;
@@ -0,0 +1,133 @@
1
+ export interface TnlConfig {
2
+ /**
3
+ * Control URL used by this project.
4
+ */
5
+ server?: string;
6
+ /**
7
+ * Team ID or unambiguous display name used by this project.
8
+ */
9
+ team?: string;
10
+ /**
11
+ * Default route and tunnel settings.
12
+ */
13
+ tunnel?: {
14
+ /**
15
+ * Complete route hostname to publish.
16
+ */
17
+ host?: string;
18
+ /**
19
+ * One DNS label beneath the current member namespace.
20
+ */
21
+ subdomain?: string;
22
+ /**
23
+ * Remove the route when this tunnel stops.
24
+ */
25
+ ephemeral?: boolean;
26
+ /**
27
+ * Allow visitors from every IP address.
28
+ */
29
+ allowAllIPs?: boolean;
30
+ /**
31
+ * Visitor IP addresses or prefixes allowed to use the route; the current client IP is added automatically.
32
+ *
33
+ * @maxItems 63
34
+ */
35
+ allowIP?: string[];
36
+ /**
37
+ * Maximum concurrent requests forwarded by the publisher for this route, including streams and upgrades.
38
+ */
39
+ requestLimit?: number;
40
+ };
41
+ publish?: Publish;
42
+ dev?: Dev;
43
+ /**
44
+ * Named local services with optional project-setting overrides.
45
+ */
46
+ services?: {
47
+ [k: string]: {
48
+ /**
49
+ * Service directory relative to the project configuration.
50
+ */
51
+ directory?: string;
52
+ /**
53
+ * Control URL override for this service.
54
+ */
55
+ server?: string;
56
+ /**
57
+ * Team override for this service.
58
+ */
59
+ team?: string;
60
+ /**
61
+ * Route and tunnel overrides for this service.
62
+ */
63
+ tunnel?: {
64
+ /**
65
+ * Complete route hostname to publish.
66
+ */
67
+ host?: string;
68
+ /**
69
+ * One DNS label beneath the current member namespace.
70
+ */
71
+ subdomain?: string;
72
+ /**
73
+ * Remove the route when this tunnel stops.
74
+ */
75
+ ephemeral?: boolean;
76
+ /**
77
+ * Allow visitors from every IP address.
78
+ */
79
+ allowAllIPs?: boolean;
80
+ /**
81
+ * Visitor IP addresses or prefixes allowed to use the route; the current client IP is added automatically.
82
+ *
83
+ * @maxItems 63
84
+ */
85
+ allowIP?: string[];
86
+ /**
87
+ * Maximum concurrent requests forwarded by the publisher for this route, including streams and upgrades.
88
+ */
89
+ requestLimit?: number;
90
+ };
91
+ publish?: Publish;
92
+ dev?: Dev;
93
+ } | undefined;
94
+ };
95
+ }
96
+ export interface Publish {
97
+ /**
98
+ * Local HTTP URL or port reached by the publisher.
99
+ */
100
+ target?: string | number;
101
+ }
102
+ export interface Dev {
103
+ /**
104
+ * Child command and arguments run by tnl dev.
105
+ *
106
+ * @minItems 1
107
+ */
108
+ command?: [string, ...string[]];
109
+ /**
110
+ * Required local service port for tnl dev.
111
+ */
112
+ port?: number;
113
+ /**
114
+ * Maximum time to wait for the local service to start.
115
+ */
116
+ startupTimeout?: string;
117
+ }
118
+ /** The Git worktree or project directory that contains tnl.config.ts. */
119
+ export interface TnlWorktree {
120
+ readonly isGit: boolean;
121
+ /** DNS-safe worktree label derived from this worktree and client state. */
122
+ readonly label: string;
123
+ readonly name: string;
124
+ readonly root: string;
125
+ }
126
+ /** Values passed to a tnl.config.ts configuration factory. */
127
+ export interface TnlConfigContext {
128
+ readonly cwd: string;
129
+ readonly env: Readonly<Record<string, string | undefined>>;
130
+ readonly worktree: TnlWorktree;
131
+ }
132
+ export type TnlConfigFactory = (context: TnlConfigContext) => TnlConfig | Promise<TnlConfig>;
133
+ export type TnlConfigInput = TnlConfig | TnlConfigFactory;
@@ -0,0 +1,2 @@
1
+ // Code generated by scripts/generate-config-types.ts. DO NOT EDIT.
2
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Adds type checking to a version-1 tnl.config.ts configuration. */
2
+ export function defineConfig(config) {
3
+ return config;
4
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { type ProjectMetadata } from "./internal/runtime.js";
2
- /** Augmented by the `.tnl/project.d.ts` file generated by the tnl client. */
2
+ /** Extended with service types from the generated `.tnl/project.d.ts` file. */
3
3
  export interface TnlProjectMetadata {
4
4
  }
5
5
  type RegisteredTnlProject = keyof TnlProjectMetadata extends never ? ProjectMetadata : Readonly<TnlProjectMetadata>;
6
6
  type TnlProject = RegisteredTnlProject & {
7
7
  readonly runningUnderTnlDev: boolean;
8
8
  };
9
- /** Validated project metadata injected by a tnl framework integration during development. */
9
+ /** Project metadata added by a tnl framework integration during development. */
10
10
  export declare const tnl: TnlProject | undefined;
11
11
  export {};
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { parseRuntimePayload } from "./internal/runtime.js";
2
2
  const serializedRuntime = typeof process === "undefined" ? undefined : process.env?.TNL_PROJECT_RUNTIME;
3
- /** Validated project metadata injected by a tnl framework integration during development. */
3
+ /** Project metadata added by a tnl framework integration during development. */
4
+ // The parser validates the runtime shape; generated project declarations supply
5
+ // project-specific service names and literals unavailable to this shared module.
4
6
  export const tnl = parseRuntimePayload(serializedRuntime);
@@ -37,5 +37,5 @@ export declare function registerLocalTarget(assignment: TnlTunnelAssignment, tar
37
37
  export declare function runtimePayload(project: ProjectMetadata, runningUnderTnlDev: 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
- /** Parses digit-only listener ports in [1, 65535], preserving source-specific diagnostics. */
40
+ /** Parses a decimal listener port from 1 through 65535. */
41
41
  export declare function parseListenerPort(value: string, source: string): number;
@@ -59,7 +59,7 @@ export function canonicalLoopbackTarget(host, port) {
59
59
  if (hostname === "::1") {
60
60
  return `http://[::1]:${port}`;
61
61
  }
62
- throw new Error("tnl development target must use a loopback listener");
62
+ throw new Error("tnl development target must listen on localhost or all interfaces");
63
63
  }
64
64
  export async function registerLocalTarget(assignment, target) {
65
65
  const body = JSON.stringify({ protocol: 1, framework: assignment.framework, target });
@@ -110,8 +110,10 @@ function parseBootstrapEnvironment(environment) {
110
110
  throw new Error(`TNL_DEV_SOCKET is required by tnl dev protocol ${protocolVersion}`);
111
111
  }
112
112
  const rawPort = environment.TNL_DEV_PORT;
113
- const port = rawPort === undefined ? undefined : parseListenerPort(rawPort, "TNL_DEV_PORT");
114
- return Object.freeze({ port, socket });
113
+ if (rawPort === undefined) {
114
+ return Object.freeze({ socket });
115
+ }
116
+ return Object.freeze({ port: parseListenerPort(rawPort, "TNL_DEV_PORT"), socket });
115
117
  }
116
118
  function discoverDevSocket(discovery, environment) {
117
119
  const getuid = process.getuid;
@@ -157,7 +159,8 @@ function parseAssignment(value, bootstrap, framework) {
157
159
  }
158
160
  const memberNamespace = requiredHostname(object.memberNamespace, "tnl dev returned member namespace");
159
161
  const hostname = requiredHostname(object.hostname, "tnl dev returned public hostname");
160
- if (object.publicURL !== `https://${hostname}`) {
162
+ const publicURL = `https://${hostname}`;
163
+ if (object.publicURL !== publicURL) {
161
164
  throw new Error("tnl dev returned an invalid public URL");
162
165
  }
163
166
  const project = parseProjectRuntime(object.project, "tnl dev project metadata");
@@ -182,7 +185,7 @@ function parseAssignment(value, bootstrap, framework) {
182
185
  hostname,
183
186
  memberNamespace,
184
187
  project,
185
- publicURL: object.publicURL,
188
+ publicURL,
186
189
  service: object.service,
187
190
  tunnelID: object.tunnelID,
188
191
  });
@@ -359,7 +362,7 @@ function absoluteNormalizedPath(value, description) {
359
362
  }
360
363
  return value;
361
364
  }
362
- /** Parses digit-only listener ports in [1, 65535], preserving source-specific diagnostics. */
365
+ /** Parses a decimal listener port from 1 through 65535. */
363
366
  export function parseListenerPort(value, source) {
364
367
  if (!/^[0-9]+$/.test(value)) {
365
368
  throw new Error(`${source} must be a port between 1 and 65535`);
@@ -383,5 +386,5 @@ function canonicalPath(value) {
383
386
  }
384
387
  }
385
388
  function isMissing(error) {
386
- return error.code === "ENOENT";
389
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
387
390
  }
@@ -0,0 +1,8 @@
1
+ interface NativeBinaryOptions {
2
+ readonly architecture?: NodeJS.Architecture;
3
+ readonly platform?: NodeJS.Platform;
4
+ readonly resolve?: (specifier: string) => string;
5
+ }
6
+ export declare function nativePackageName(platform: NodeJS.Platform, architecture: NodeJS.Architecture): string;
7
+ export declare function resolveNativeBinary(options?: NativeBinaryOptions): string;
8
+ export {};
@@ -0,0 +1,55 @@
1
+ import { constants, accessSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { nativeTargets } from "./native-targets.js";
6
+ const require = createRequire(import.meta.url);
7
+ const launcherManifest = new URL("../../package.json", import.meta.url);
8
+ const nativePackages = new Map(nativeTargets.map(({ platform, architecture, packageName }) => [
9
+ `${platform}-${architecture}`,
10
+ packageName,
11
+ ]));
12
+ export function nativePackageName(platform, architecture) {
13
+ const packageName = nativePackages.get(`${platform}-${architecture}`);
14
+ if (packageName === undefined) {
15
+ throw new Error(`unsupported platform ${platform}-${architecture}; tnl supports macOS and Linux on arm64 and x64`);
16
+ }
17
+ return packageName;
18
+ }
19
+ export function resolveNativeBinary(options = {}) {
20
+ const { architecture = process.arch, platform = process.platform, resolve = require.resolve, } = options;
21
+ const packageName = nativePackageName(platform, architecture);
22
+ let nativeManifestPath;
23
+ try {
24
+ nativeManifestPath = resolve(`${packageName}/package.json`);
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND") {
28
+ throw new Error(`${packageName} is missing; reinstall @tnldotdev/tnl without disabling optional dependencies`, { cause: error });
29
+ }
30
+ throw error;
31
+ }
32
+ const mainManifest = readManifest(launcherManifest);
33
+ const nativeManifest = readManifest(nativeManifestPath);
34
+ if (nativeManifest.version !== mainManifest.version) {
35
+ throw new Error(`${packageName}@${String(nativeManifest.version)} does not match @tnldotdev/tnl@${String(mainManifest.version)}`);
36
+ }
37
+ const binary = path.join(path.dirname(nativeManifestPath), "bin", "tnl");
38
+ try {
39
+ accessSync(binary, constants.X_OK);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`${packageName} does not contain an executable tnl binary`, { cause: error });
43
+ }
44
+ return binary;
45
+ }
46
+ function readManifest(file) {
47
+ const value = JSON.parse(readFileSync(file, "utf8"));
48
+ if (value === null ||
49
+ typeof value !== "object" ||
50
+ !("version" in value) ||
51
+ typeof value.version !== "string") {
52
+ throw new Error("package manifest has an invalid shape");
53
+ }
54
+ return { version: value.version };
55
+ }
@@ -0,0 +1,9 @@
1
+ export type NativeArchitecture = "arm64" | "x64";
2
+ export type NativePlatform = "darwin" | "linux";
3
+ export interface NativeTarget {
4
+ readonly architecture: NativeArchitecture;
5
+ readonly packageName: `@tnldotdev/tnl-${NativePlatform}-${NativeArchitecture}`;
6
+ readonly platform: NativePlatform;
7
+ }
8
+ /** Internal distribution catalog shared by the launcher and release scripts. */
9
+ export declare const nativeTargets: readonly Readonly<NativeTarget>[];
@@ -1,12 +1,8 @@
1
- /** Internal distribution catalog shared by the launcher and release scripts.
2
- * Node architecture names belong here; Go artifact paths and versions do not.
3
- * Entries retain native publish order. This module is not a public package export.
4
- */
5
- export const nativeTargets = Object.freeze(
6
- [
1
+ const targets = [
7
2
  { platform: "darwin", architecture: "arm64", packageName: "@tnldotdev/tnl-darwin-arm64" },
8
3
  { platform: "darwin", architecture: "x64", packageName: "@tnldotdev/tnl-darwin-x64" },
9
4
  { platform: "linux", architecture: "arm64", packageName: "@tnldotdev/tnl-linux-arm64" },
10
5
  { platform: "linux", architecture: "x64", packageName: "@tnldotdev/tnl-linux-x64" },
11
- ].map((target) => Object.freeze(target)),
12
- );
6
+ ];
7
+ /** Internal distribution catalog shared by the launcher and release scripts. */
8
+ export const nativeTargets = Object.freeze(targets.map((target) => Object.freeze(target)));
@@ -5,14 +5,14 @@ export interface ProjectServiceMetadata {
5
5
  }
6
6
  export interface ProjectMetadata {
7
7
  readonly memberNamespace: string;
8
- readonly services: Readonly<Record<string, ProjectServiceMetadata>>;
8
+ readonly services: Readonly<Record<string, ProjectServiceMetadata | undefined>>;
9
9
  }
10
10
  export interface ProjectRuntime extends ProjectMetadata {
11
11
  readonly runningUnderTnlDev: boolean;
12
12
  }
13
- /** Validates and deep-freezes public metadata, excluding runtime flags and Node-only discovery fields. */
13
+ /** Checks browser-safe project metadata and makes it read-only. */
14
14
  export declare function parseProjectMetadata(value: unknown, description: string): ProjectMetadata;
15
- /** Matches the native client's 1-32 byte ASCII service-name grammar without normalization. */
15
+ /** Reports whether a value is a valid 1-32 character ASCII service name. */
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;
@@ -1,7 +1,7 @@
1
1
  const maximumRuntimeBytes = 64 * 1024;
2
2
  const maximumServices = 32;
3
3
  const serviceNamePattern = /^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
4
- /** Validates and deep-freezes public metadata, excluding runtime flags and Node-only discovery fields. */
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
7
  exactKeys(object, ["memberNamespace", "services"], description);
@@ -21,7 +21,8 @@ export function parseProjectMetadata(value, description) {
21
21
  exactKeys(service, ["hostname", "memberNamespace", "url"], `${description} service ${JSON.stringify(name)}`);
22
22
  const serviceMemberNamespace = requiredHostname(service.memberNamespace, `${description} service ${JSON.stringify(name)} member namespace`);
23
23
  const hostname = requiredHostname(service.hostname, `${description} service ${JSON.stringify(name)} hostname`);
24
- if (service.url !== `https://${hostname}`) {
24
+ const url = `https://${hostname}`;
25
+ if (service.url !== url) {
25
26
  throw new Error(`${description} service ${JSON.stringify(name)} has an invalid URL`);
26
27
  }
27
28
  if (hostnames.has(hostname)) {
@@ -31,7 +32,7 @@ export function parseProjectMetadata(value, description) {
31
32
  services[name] = Object.freeze({
32
33
  memberNamespace: serviceMemberNamespace,
33
34
  hostname,
34
- url: service.url,
35
+ url,
35
36
  });
36
37
  }
37
38
  return Object.freeze({
@@ -39,7 +40,7 @@ export function parseProjectMetadata(value, description) {
39
40
  services: Object.freeze(services),
40
41
  });
41
42
  }
42
- /** Matches the native client's 1-32 byte ASCII service-name grammar without normalization. */
43
+ /** Reports whether a value is a valid 1-32 character ASCII service name. */
43
44
  export function validServiceName(value) {
44
45
  return typeof value === "string" && serviceNamePattern.test(value);
45
46
  }
package/dist/next.d.ts CHANGED
@@ -6,5 +6,5 @@ export interface NextConfigContext {
6
6
  /** A function that returns Next.js configuration. */
7
7
  export type NextConfigFactory = (phase: string, context: NextConfigContext) => NextConfig | Promise<NextConfig>;
8
8
  export type NextConfigInput = NextConfig | Promise<NextConfig> | NextConfigFactory;
9
- /** Adds tnl project metadata and safe `tnl dev` routing to a Next.js development server. */
9
+ /** Configures a Next.js development server for `tnl dev` and adds project metadata. */
10
10
  export declare function withTnl(config?: NextConfigInput, ...extra: never[]): NextConfigFactory;
package/dist/next.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { canonicalLoopbackTarget, parseListenerPort, readDevelopmentContext, registerLocalTarget, requestTunnelAssignment, runtimePayload, } from "./internal/dev.js";
2
2
  const developmentServerPhase = "phase-development-server";
3
3
  const runtimeEnvironmentName = "TNL_PROJECT_RUNTIME";
4
- /** Adds tnl project metadata and safe `tnl dev` routing to a Next.js development server. */
4
+ /** Configures a Next.js development server for `tnl dev` and adds project metadata. */
5
5
  export function withTnl(config = {}, ...extra) {
6
6
  if (extra.length !== 0) {
7
7
  throw new Error("withTnl() does not accept tunnel options; use project configuration");
package/dist/vite.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { Plugin } from "vite";
2
- /** Adds tnl project metadata and safe `tnl dev` routing to a Vite development server. */
2
+ /** Configures a Vite development server for `tnl dev` and adds project metadata. */
3
3
  export default function tnl(...arguments_: never[]): Plugin;
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { canonicalLoopbackTarget, readDevelopmentContext, registerLocalTarget, requestTunnelAssignment, runtimePayload, } from "./internal/dev.js";
2
2
  const runtimeDefineName = "process.env.TNL_PROJECT_RUNTIME";
3
- /** Adds tnl project metadata and safe `tnl dev` routing to a Vite development server. */
3
+ /** Configures a Vite development server for `tnl dev` and adds project metadata. */
4
4
  export default function tnl(...arguments_) {
5
5
  if (arguments_.length !== 0) {
6
6
  throw new Error("tnl() does not accept tunnel options; use project configuration");
package/package.json CHANGED
@@ -1,25 +1,9 @@
1
1
  {
2
2
  "name": "@tnldotdev/tnl",
3
- "version": "0.1.0-rc.25",
4
- "description": "The tnl client and framework integrations for project-local development.",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/tnldotdev/tnl.git",
9
- "directory": "packages/tnl"
10
- },
3
+ "version": "0.1.0-rc.27",
11
4
  "bin": {
12
- "tnl": "bin/tnl.mjs"
5
+ "tnl": "dist/bin/tnl.js"
13
6
  },
14
- "files": [
15
- "bin",
16
- "dist",
17
- "lib",
18
- "NOTICE",
19
- "THIRD_PARTY_LICENSES.txt"
20
- ],
21
- "type": "module",
22
- "sideEffects": false,
23
7
  "exports": {
24
8
  ".": {
25
9
  "types": "./dist/index.d.ts",
@@ -27,8 +11,9 @@
27
11
  "default": "./dist/index.js"
28
12
  },
29
13
  "./config": {
30
- "types": "./lib/config.d.ts",
31
- "import": "./lib/config.mjs"
14
+ "types": "./dist/config.d.ts",
15
+ "import": "./dist/config.js",
16
+ "default": "./dist/config.js"
32
17
  },
33
18
  "./next": {
34
19
  "types": "./dist/next.d.ts",
@@ -41,19 +26,33 @@
41
26
  "default": "./dist/vite.js"
42
27
  }
43
28
  },
44
- "publishConfig": {
45
- "access": "public"
46
- },
47
29
  "scripts": {
48
30
  "build": "pnpm run clean && tsc --project tsconfig.json",
49
- "clean": "node ../../scripts/clean-package.mjs",
31
+ "clean": "node ../../scripts/clean-package.ts",
50
32
  "prepack": "pnpm run build"
51
33
  },
34
+ "description": "The tnl client and framework integrations for project-local development.",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/tnldotdev/tnl.git",
39
+ "directory": "packages/tnl"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "NOTICE",
44
+ "THIRD_PARTY_LICENSES.txt"
45
+ ],
46
+ "type": "module",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
52
51
  "devDependencies": {
53
- "next": "16.3.4",
54
- "react": "19.2.8",
55
- "react-dom": "19.2.8",
56
- "vite": "8.2.2"
52
+ "next": "16.3.5",
53
+ "react": "19.3.0",
54
+ "react-dom": "19.3.0",
55
+ "vite": "8.3.0"
57
56
  },
58
57
  "peerDependencies": {
59
58
  "next": ">=16.3.4",
@@ -71,12 +70,12 @@
71
70
  "node": ">=22.18"
72
71
  },
73
72
  "optionalDependencies": {
74
- "@tnldotdev/tnl-darwin-arm64": "0.1.0-rc.25",
75
- "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.25",
76
- "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.25",
77
- "@tnldotdev/tnl-linux-x64": "0.1.0-rc.25"
73
+ "@tnldotdev/tnl-darwin-arm64": "0.1.0-rc.27",
74
+ "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.27",
75
+ "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.27",
76
+ "@tnldotdev/tnl-linux-x64": "0.1.0-rc.27"
78
77
  },
79
78
  "tnl": {
80
- "commit": "a9b3066ab2d1a71b06655b983894b96926e27452"
79
+ "commit": "f446e681bf5e70f9170b082aea9518681cddf532"
81
80
  }
82
81
  }
package/readme.md ADDED
@@ -0,0 +1,177 @@
1
+ # `@tnldotdev/tnl`
2
+
3
+ This package installs the native `tnl` client and provides project configuration
4
+ types, a browser-safe project runtime, and official Next.js and Vite
5
+ integrations. It does not include the `tnld` server process.
6
+
7
+ ## install
8
+
9
+ ```console
10
+ pnpm add --save-dev @tnldotdev/tnl@next
11
+ ```
12
+
13
+ The package selects an exact-version native dependency for macOS or Linux on
14
+ arm64 or x64. It does not run an install script or download executable code from
15
+ another host. Node.js 22.18 or newer is required.
16
+
17
+ The native client enables pseudonymous telemetry by default. Disable it with
18
+ `TNL_NO_TELEMETRY=true` or `--no-telemetry`; see the
19
+ [telemetry disclosure](../../docs/cli-reference.md#telemetry).
20
+
21
+ ## initialize a project
22
+
23
+ Run the initializer from your project root:
24
+
25
+ ```console
26
+ pnpm exec tnl init
27
+ ```
28
+
29
+ The initializer detects supported package managers and frameworks. It creates
30
+ missing tnl and framework configuration, preserves existing configuration, and
31
+ lists any changes you need to make yourself.
32
+
33
+ Once you've completed those steps, start your app:
34
+
35
+ ```console
36
+ pnpm exec tnl dev
37
+ ```
38
+
39
+ tnl starts the configured command and prints its HTTPS URL. Sign in when
40
+ prompted; tnl uses the hosted service by default. The integration follows the
41
+ port your app actually uses, even if another app has taken its preferred port.
42
+ Live reload works through the URL.
43
+
44
+ Each Git worktree gets its own URL by default. Restarting the same worktree
45
+ with the same local tnl state reuses the URL. See the integration examples for
46
+ [Next.js](#integrate-nextjs) and [Vite](#integrate-vite).
47
+
48
+ Keep service names, commands, targets, route policy, and team selection in
49
+ [project configuration](../../docs/project-configuration.md). Framework
50
+ integrations do not accept separate route options.
51
+
52
+ ## generate project metadata
53
+
54
+ Sign in, then generate project metadata:
55
+
56
+ ```console
57
+ tnl login
58
+ tnl config generate
59
+ ```
60
+
61
+ The command writes:
62
+
63
+ ```text
64
+ .tnl/project.json browser-safe project and service addresses
65
+ .tnl/project.d.ts exact TypeScript service and hostname types
66
+ ```
67
+
68
+ Add `.tnl/` to `.gitignore`. Regenerate after changing services, servers, teams,
69
+ or domains. `tnl dev` also regenerates metadata when the project has a
70
+ configuration file.
71
+
72
+ Include the declaration in the application's existing TypeScript inputs. For a
73
+ service in `apps/web`, for example:
74
+
75
+ ```json
76
+ {
77
+ "include": ["**/*.ts", "**/*.tsx", "../../.tnl/project.d.ts"]
78
+ }
79
+ ```
80
+
81
+ Keep every other entry required by the framework.
82
+
83
+ ## read project metadata
84
+
85
+ Framework integrations expose the generated, browser-safe metadata through the
86
+ root package export:
87
+
88
+ ```ts
89
+ import { tnl } from "@tnldotdev/tnl";
90
+
91
+ if (tnl) {
92
+ tnl.memberNamespace;
93
+ tnl.services.api.hostname;
94
+ tnl.services.api.url;
95
+ tnl.runningUnderTnlDev;
96
+ }
97
+ ```
98
+
99
+ The value describes assigned addresses. It does not prove that a route or local
100
+ service is currently healthy.
101
+
102
+ | Development context | Result |
103
+ | ---------------------------------------------- | -------------------------------------------------------- |
104
+ | No metadata and no `tnl dev` environment | `tnl` is undefined. |
105
+ | Metadata without a matching development socket | Metadata is available and `runningUnderTnlDev` is false. |
106
+ | A matching `tnl dev` environment or socket | Metadata is available and `runningUnderTnlDev` is true. |
107
+ | Build, preview, or production | `tnl` is undefined. |
108
+
109
+ The values are read-only. Malformed metadata causes an error. No control access
110
+ token or saved login is included.
111
+
112
+ ## integrate next.js
113
+
114
+ Next.js 16.3.4 or newer is supported.
115
+
116
+ ```ts
117
+ // next.config.ts
118
+ import { withTnl } from "@tnldotdev/tnl/next";
119
+
120
+ export default withTnl({
121
+ reactStrictMode: true,
122
+ });
123
+ ```
124
+
125
+ `withTnl` accepts a configuration object, promise, or synchronous or asynchronous
126
+ configuration function. During `tnl dev`, it:
127
+
128
+ - adds the assigned hostname to `allowedDevOrigins`;
129
+ - injects project metadata;
130
+ - reports the actual listener after Next.js binds.
131
+
132
+ The integration preserves existing host and port settings. Without
133
+ `tnl dev --port`, Next.js can choose another port when its preferred port is
134
+ occupied. With `--port`, the reported port must match exactly.
135
+
136
+ ## integrate vite
137
+
138
+ Vite 6.0.9 or newer is supported.
139
+
140
+ ```ts
141
+ // vite.config.ts
142
+ import { defineConfig } from "vite";
143
+ import tnl from "@tnldotdev/tnl/vite";
144
+
145
+ export default defineConfig({
146
+ plugins: [tnl()],
147
+ });
148
+ ```
149
+
150
+ During `tnl dev`, the plugin:
151
+
152
+ - adds the assigned hostname to Vite's allowed hosts;
153
+ - injects project metadata;
154
+ - reports the actual listener after Vite binds.
155
+
156
+ Vite keeps its normal host and port behavior, including next-port fallback. A
157
+ port set by `tnl dev --port` must match exactly. Middleware mode is not
158
+ supported because it does not provide a listener to register.
159
+
160
+ ## run the framework separately
161
+
162
+ The framework does not have to be a child of `tnl dev`. You can start
163
+ `tnl dev web` in one terminal and start the framework from the service directory
164
+ in another.
165
+
166
+ The integration looks for the matching private development socket when the
167
+ framework configuration loads. It does not keep searching afterward. Start
168
+ `tnl dev` first when you use this workflow.
169
+
170
+ ## listener requirements
171
+
172
+ The listener must accept HTTP on localhost. A wildcard binding such as
173
+ `0.0.0.0` or `::` is allowed because it includes localhost. Binding only to a
174
+ specific LAN address is not supported.
175
+
176
+ Host settings remain independent. Your framework can still listen on the LAN,
177
+ but tnl does not require or enable that exposure.
package/README.md DELETED
@@ -1,196 +0,0 @@
1
- # `@tnldotdev/tnl`
2
-
3
- This package installs the `tnl` client for macOS or Linux on arm64 or x64 and
4
- provides its browser-safe project runtime plus official Next.js and Vite
5
- integrations. It does not include the `tnld` server process.
6
-
7
- ## Install
8
-
9
- ```console
10
- pnpm add --save-dev @tnldotdev/tnl@next
11
- ```
12
-
13
- The package selects an exact-version native optional dependency for the current
14
- platform. It has no install script and does not download executable code from a
15
- third-party host. Node.js 22.18 or newer is required by the launcher,
16
- integrations, and TypeScript configuration loader.
17
-
18
- The native CLI enables pseudonymous telemetry by default. Use
19
- `TNL_NO_TELEMETRY=true` or `--no-telemetry` to disable it; see the
20
- [telemetry disclosure](../../README.md#telemetry).
21
-
22
- ## Configuration
23
-
24
- Run `tnl init` to add the package and missing project/framework configuration.
25
- It preserves existing Next.js, Vite, and TypeScript configuration and reports
26
- remaining integration actions. Configure an integration below before starting
27
- a framework-discovered tunnel.
28
-
29
- For a project with existing `apps/api` and `apps/web` directories:
30
-
31
- ```ts
32
- // tnl.config.ts
33
- import { defineConfig } from "@tnldotdev/tnl/config";
34
-
35
- export default defineConfig({
36
- dev: { command: ["pnpm", "dev"] },
37
- services: {
38
- api: { directory: "apps/api", publish: { target: 3001 } },
39
- web: { directory: "apps/web" },
40
- },
41
- });
42
- ```
43
-
44
- Each service overrides root defaults. Commands run in the service directory,
45
- which must exist within the project root. Choose explicitly when several
46
- services exist: `tnl dev web` or `tnl publish api`. A single service is selected
47
- automatically. Service names are 1-32 lowercase ASCII letters/digits/hyphens,
48
- begin with a letter, and cannot end with a hyphen; at most 32 services are allowed.
49
-
50
- `defineConfig` also accepts a synchronous or asynchronous factory:
51
-
52
- ```ts
53
- export default defineConfig(({ worktree }) => ({
54
- tunnel: { subdomain: worktree.label },
55
- dev: { command: ["pnpm", "dev"], startupTimeout: "90s" },
56
- }));
57
- ```
58
-
59
- `worktree.label` combines a readable name with an eight-character hash, stable
60
- for one client state directory but distinct across worktrees and installations.
61
- Its private random input is not exposed. Factories receive deeply frozen
62
- `cwd`, `env`, and `worktree` context. `cwd` is the invocation directory; Node
63
- executes from the configuration directory. Both the loader environment and
64
- `context.env` omit `TNL_*` and `TNLD_*`, not arbitrary application secrets.
65
-
66
- Configuration executes trusted project code, not a sandbox. `defineConfig` is
67
- type assistance, not runtime validation; the native client validates the result.
68
- TypeScript uses camel-case fields and implicitly version 1. Static YAML/JSON
69
- requires `version: 1` and snake-case fields; see
70
- [discovery and precedence](../../README.md#project-configuration) and the
71
- [JSON Schema](https://tnl.dev/schema/v1.json).
72
-
73
- `tunnel.host` and `tunnel.subdomain` are alternatives, as are `public: true` and
74
- `allowIP`. Service overrides replace the corresponding inherited alternative.
75
- `dev.port` forces the exact listener port. `dev.startupTimeout` defaults to two
76
- minutes and must be positive and at most ten minutes. `dev.command` is an
77
- argument array, not a shell command string.
78
-
79
- ## Project Runtime
80
-
81
- Authenticate to the configured server, then generate metadata from the project
82
- root:
83
-
84
- ```console
85
- tnl login https://control.tnl.example.com --token
86
- tnl config generate
87
- ```
88
-
89
- Generation resolves the authenticated membership and ready domain for the root
90
- and every service, including services with server/team overrides. It writes
91
- `.tnl/project.json` and `.tnl/project.d.ts`. Regenerate after changing service,
92
- server, team, or domain configuration; `tnl dev` also generates metadata when
93
- project configuration is present. Keep `.tnl` ignored by Git.
94
-
95
- Add the declaration to the application's existing TypeScript `include` list.
96
- For an app at the project root, include `.tnl/project.d.ts`; for the example's
97
- `apps/web/tsconfig.json`, include `../../.tnl/project.d.ts`:
98
-
99
- ```json
100
- {
101
- "include": ["**/*.ts", "**/*.tsx", "../../.tnl/project.d.ts"]
102
- }
103
- ```
104
-
105
- Preserve other framework-required includes. The augmentation gives exact
106
- service keys and literal hostname/URL types. Without it, types remain broad;
107
- check that a service exists before accessing it.
108
-
109
- The integrations expose this browser-safe runtime during development:
110
-
111
- ```ts
112
- import { tnl } from "@tnldotdev/tnl";
113
-
114
- if (tnl) {
115
- tnl.memberNamespace;
116
- tnl.services.api.hostname;
117
- tnl.services.api.url;
118
- tnl.runningUnderTnlDev;
119
- }
120
- ```
121
-
122
- The `tnl` value is undefined during builds, previews, and production. During
123
- development, behavior depends on discovery:
124
-
125
- | Development context | Runtime and network behavior |
126
- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
127
- | No generated metadata or explicit bootstrap | `tnl` is undefined; no tunnel configuration |
128
- | Metadata without a matching development socket | Frozen metadata, `runningUnderTnlDev: false`; no tunnel configuration |
129
- | Explicit bootstrap or discovered matching `tnl dev` socket | Assigned metadata, `runningUnderTnlDev: true`; configure and register the actual listener |
130
-
131
- Socket discovery supports starting `tnl dev web` first and the framework from
132
- its service directory in another terminal. Discovery happens when framework
133
- configuration loads, not continuously. Metadata is a snapshot, not route
134
- readiness or service health. Values are deeply frozen; malformed metadata throws
135
- rather than silently becoming undefined. `tnl publish` cannot inject metadata
136
- into an already-running application.
137
-
138
- ## Next.js
139
-
140
- ```ts
141
- // next.config.ts
142
- import { withTnl } from "@tnldotdev/tnl/next";
143
-
144
- export default withTnl({
145
- reactStrictMode: true,
146
- });
147
- ```
148
-
149
- `withTnl` preserves object, promised, synchronous-function, and
150
- asynchronous-function configuration. During `tnl dev`, it adds the assigned
151
- hostname to `allowedDevOrigins`, injects the project runtime, and registers the
152
- actual listener target reported by Next.js after it binds. Existing host and
153
- port choices, including Next.js defaults and custom values, are preserved. An
154
- unforced port may use Next.js's normal occupied-port retry behavior without
155
- tunneling a different process on the preferred port. A port forced by
156
- `tnl dev --port` remains exact. Host settings remain independent, so a project
157
- can intentionally expose its development server on the LAN as well as through
158
- tnl.
159
-
160
- Next.js 16.3.4 or newer is supported.
161
-
162
- ## Vite
163
-
164
- ```ts
165
- // vite.config.ts
166
- import { defineConfig } from "vite";
167
- import tnl from "@tnldotdev/tnl/vite";
168
-
169
- export default defineConfig({
170
- plugins: [tnl()],
171
- });
172
- ```
173
-
174
- During `tnl dev`, the plugin allows the assigned hostname, injects the project
175
- runtime, and registers Vite's actual post-bind target. It preserves Vite's
176
- default or configured host and port behavior, including occupied-port retries;
177
- a port forced by `tnl dev --port` remains exact. User host settings can
178
- independently expose Vite on the LAN. Without a development socket the plugin
179
- only injects generated metadata. Builds and previews remain inert. Vite 6.0.9
180
- or newer is supported.
181
-
182
- Next.js and Vite are optional peers, so only the framework already used by the
183
- project is required. Keep hostname, policy, server, service, and command
184
- settings in project configuration rather than passing integration options.
185
-
186
- ### Listener Requirements
187
-
188
- The target must be loopback HTTP. Wildcard bindings (`0.0.0.0` or `::`) allow
189
- LAN exposure while tnl connects through loopback; binding only a specific LAN
190
- address is rejected. Vite middleware mode has no supported listening target.
191
- Next.js must report an HTTP listener origin. Forced ports are checked against
192
- the actual listener, not assumed from configuration. Vite's `allowedHosts: true`
193
- is preserved; the integration does not re-enable host filtering you disabled.
194
-
195
- Deploy `tnld` with the release container or install it from Homebrew or a
196
- release archive.
package/bin/tnl.mjs DELETED
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import process from "node:process";
4
- import { resolveNativeBinary } from "../lib/launcher.mjs";
5
-
6
- try {
7
- const binary = resolveNativeBinary();
8
- process.execve(binary, [binary, ...process.argv.slice(2)], process.env);
9
- } catch (error) {
10
- const message = error instanceof Error ? error.message : String(error);
11
- process.stderr.write(`tnl: ${message}\n`);
12
- process.exitCode = 1;
13
- }
package/lib/config.d.ts DELETED
@@ -1,69 +0,0 @@
1
- export interface TNL {
2
- server?: string;
3
- team?: string;
4
- tunnel?: {
5
- host?: string;
6
- subdomain?: string;
7
- public?: boolean;
8
- ephemeral?: boolean;
9
- /**
10
- * @maxItems 63
11
- */
12
- allowIP?: string[];
13
- };
14
- publish?: Publish;
15
- dev?: Dev;
16
- services?: {
17
- [k: string]: {
18
- directory?: string;
19
- server?: string;
20
- team?: string;
21
- tunnel?: {
22
- host?: string;
23
- subdomain?: string;
24
- public?: boolean;
25
- ephemeral?: boolean;
26
- /**
27
- * @maxItems 63
28
- */
29
- allowIP?: string[];
30
- };
31
- publish?: Publish;
32
- dev?: Dev;
33
- };
34
- };
35
- }
36
- export interface Publish {
37
- target?: string | number;
38
- }
39
- export interface Dev {
40
- /**
41
- * @minItems 1
42
- */
43
- command?: [string, ...string[]];
44
- port?: number;
45
- startupTimeout?: string;
46
- }
47
-
48
- /** Details about the Git worktree or project directory containing tnl.config.ts. */
49
- export interface TnlWorktree {
50
- readonly isGit: boolean;
51
- /** DNS-safe name keyed to this worktree and client state directory. */
52
- readonly label: string;
53
- readonly name: string;
54
- readonly root: string;
55
- }
56
-
57
- /** Values available to a tnl.config.ts configuration factory. */
58
- export interface TnlConfigContext {
59
- readonly cwd: string;
60
- readonly env: Readonly<Record<string, string>>;
61
- readonly worktree: TnlWorktree;
62
- }
63
-
64
- export type TnlConfigFactory = (context: TnlConfigContext) => TNL | Promise<TNL>;
65
-
66
- export type TnlConfigInput = TNL | TnlConfigFactory;
67
-
68
- /** Provides type checking for an implicit-version-1 tnl.config.ts configuration. */
69
- export declare function defineConfig<const Config extends TnlConfigInput>(config: Config): Config;
package/lib/config.mjs DELETED
@@ -1,3 +0,0 @@
1
- export function defineConfig(config) {
2
- return config;
3
- }
package/lib/launcher.mjs DELETED
@@ -1,65 +0,0 @@
1
- import { constants, accessSync, readFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- import process from "node:process";
5
- import { nativeTargets } from "./native-targets.mjs";
6
-
7
- const require = createRequire(import.meta.url);
8
- const launcherManifest = new URL("../package.json", import.meta.url);
9
-
10
- const nativePackages = new Map(
11
- nativeTargets.map(({ platform, architecture, packageName }) => [
12
- `${platform}-${architecture}`,
13
- packageName,
14
- ]),
15
- );
16
-
17
- export function nativePackageName(platform, architecture) {
18
- const packageName = nativePackages.get(`${platform}-${architecture}`);
19
- if (packageName === undefined) {
20
- throw new Error(
21
- `unsupported platform ${platform}-${architecture}; tnl supports macOS and Linux on arm64 and x64`,
22
- );
23
- }
24
- return packageName;
25
- }
26
-
27
- export function resolveNativeBinary({
28
- architecture = process.arch,
29
- platform = process.platform,
30
- resolve = require.resolve,
31
- } = {}) {
32
- const packageName = nativePackageName(platform, architecture);
33
- let nativeManifestPath;
34
- try {
35
- nativeManifestPath = resolve(`${packageName}/package.json`);
36
- } catch (error) {
37
- if (error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND") {
38
- throw new Error(
39
- `${packageName} is missing; reinstall @tnldotdev/tnl without disabling optional dependencies`,
40
- { cause: error },
41
- );
42
- }
43
- throw error;
44
- }
45
-
46
- const mainManifest = readManifest(launcherManifest);
47
- const nativeManifest = readManifest(nativeManifestPath);
48
- if (nativeManifest.version !== mainManifest.version) {
49
- throw new Error(
50
- `${packageName}@${String(nativeManifest.version)} does not match @tnldotdev/tnl@${String(mainManifest.version)}`,
51
- );
52
- }
53
-
54
- const binary = path.join(path.dirname(nativeManifestPath), "bin", "tnl");
55
- try {
56
- accessSync(binary, constants.X_OK);
57
- } catch (error) {
58
- throw new Error(`${packageName} does not contain an executable tnl binary`, { cause: error });
59
- }
60
- return binary;
61
- }
62
-
63
- function readManifest(file) {
64
- return JSON.parse(readFileSync(file, "utf8"));
65
- }