@tnldotdev/tnl 0.1.0-rc.26 → 0.1.0-rc.28

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.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
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);
@@ -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
  });
@@ -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,7 +5,7 @@ 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;
@@ -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({
package/package.json CHANGED
@@ -1,25 +1,9 @@
1
1
  {
2
2
  "name": "@tnldotdev/tnl",
3
- "version": "0.1.0-rc.26",
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.28",
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.26",
75
- "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.26",
76
- "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.26",
77
- "@tnldotdev/tnl-linux-x64": "0.1.0-rc.26"
73
+ "@tnldotdev/tnl-darwin-arm64": "0.1.0-rc.28",
74
+ "@tnldotdev/tnl-darwin-x64": "0.1.0-rc.28",
75
+ "@tnldotdev/tnl-linux-arm64": "0.1.0-rc.28",
76
+ "@tnldotdev/tnl-linux-x64": "0.1.0-rc.28"
78
77
  },
79
78
  "tnl": {
80
- "commit": "d6e8318fb2738a4377e67856e9eb6fe017e6097a"
79
+ "commit": "48180dc541348df42a0730cdaf749a2d507b05f6"
81
80
  }
82
81
  }
package/readme.md ADDED
@@ -0,0 +1,25 @@
1
+ # `@tnldotdev/tnl`
2
+
3
+ The tnl client, plus official Next.js and Vite integrations for project-local
4
+ development. Available for macOS and Linux on arm64 and x64 with Node.js 22.18
5
+ or newer. The package selects a native binary through optional dependencies;
6
+ it does not install the `tnld` server.
7
+
8
+ ```console
9
+ pnpm add -D @tnldotdev/tnl@next
10
+ pnpm exec tnl init
11
+ pnpm exec tnl dev
12
+ ```
13
+
14
+ `tnl init` sets up missing project and framework configuration and lists any
15
+ actions required for existing files. Sign in when prompted; hosted tnl.dev is
16
+ the default server. Each Git worktree gets its own HTTPS URL.
17
+
18
+ Read the [quickstart](https://tnl.dev/docs),
19
+ [Next.js and Vite setup](https://tnl.dev/docs/frameworks),
20
+ [project configuration](https://tnl.dev/docs/configuration), and
21
+ [CLI commands](https://tnl.dev/docs/cli) on tnl.dev.
22
+
23
+ The client enables pseudonymous telemetry by default. Disable it with
24
+ `TNL_NO_TELEMETRY=true` or `--no-telemetry`; see the
25
+ [telemetry disclosure](https://tnl.dev/docs/cli#telemetry).
package/README.md DELETED
@@ -1,212 +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 project service inherits the root settings and can override them. Commands
45
- run in the service directory, which must be inside the project root. If the
46
- project has several services, choose one with a command such as `tnl dev web` or
47
- `tnl publish api`. A project with one service selects it automatically.
48
-
49
- Service names must start with a lowercase ASCII letter. They may contain
50
- lowercase letters, digits, and hyphens, cannot end with a hyphen, and may be up
51
- to 32 characters long. A project may define up to 32 services.
52
-
53
- `defineConfig` also accepts a synchronous or asynchronous factory:
54
-
55
- ```ts
56
- export default defineConfig(({ worktree }) => ({
57
- tunnel: { subdomain: worktree.label },
58
- dev: { command: ["pnpm", "dev"], startupTimeout: "90s" },
59
- }));
60
- ```
61
-
62
- `worktree.label` combines a readable name with an eight-character hash. It stays
63
- the same for a worktree and client state directory, but differs across worktrees
64
- and installations. The private value used to create the hash is not exposed.
65
-
66
- Factories receive read-only `cwd`, `env`, and `worktree` values. `cwd` is the
67
- directory where the command started. Node runs the configuration file from its
68
- own directory. The loader and `context.env` omit `TNL_*` and `TNLD_*` variables,
69
- but they do not remove other application secrets.
70
-
71
- TypeScript configuration runs as trusted project code. It is not sandboxed.
72
- `defineConfig` provides type checking; the native client still validates the
73
- result at runtime. TypeScript uses camel-case fields and is always version 1.
74
- Static YAML and JSON require `version: 1` and snake-case fields; see
75
- [discovery and precedence](../../README.md#project-configuration) and the
76
- [JSON Schema](https://tnl.dev/schema/v1.json).
77
-
78
- Choose either `tunnel.host` or `tunnel.subdomain`. Choose either `public: true`
79
- or `allowIP`. A project-service override replaces the other inherited choice.
80
- `dev.port` requires the local service to use that port. `dev.startupTimeout`
81
- defaults to two minutes and must be greater than zero and no more than ten
82
- minutes. `dev.command` is an argument array, not a shell command string.
83
-
84
- ## Project Runtime
85
-
86
- Authenticate to the configured server, then generate metadata from the project
87
- root:
88
-
89
- ```console
90
- tnl login https://control.tnl.example.com --token
91
- tnl config generate
92
- ```
93
-
94
- Generation selects the current membership and a ready domain for the project and
95
- each service. It also respects service-specific server and team settings. The
96
- command writes `.tnl/project.json` and `.tnl/project.d.ts`.
97
-
98
- Regenerate after changing project services, servers, teams, or domains. The
99
- `tnl dev` command also generates metadata when the project has a configuration
100
- file. Keep `.tnl` ignored by Git.
101
-
102
- Add the declaration to the application's existing TypeScript `include` list.
103
- For an app at the project root, include `.tnl/project.d.ts`; for the example's
104
- `apps/web/tsconfig.json`, include `../../.tnl/project.d.ts`:
105
-
106
- ```json
107
- {
108
- "include": ["**/*.ts", "**/*.tsx", "../../.tnl/project.d.ts"]
109
- }
110
- ```
111
-
112
- Keep any other entries required by the framework. The generated declaration
113
- adds the configured service names and their exact hostname and URL types. Without
114
- it, service names use a general string type, so check that a service exists
115
- before reading it.
116
-
117
- The integrations expose this browser-safe runtime during development:
118
-
119
- ```ts
120
- import { tnl } from "@tnldotdev/tnl";
121
-
122
- if (tnl) {
123
- tnl.memberNamespace;
124
- tnl.services.api.hostname;
125
- tnl.services.api.url;
126
- tnl.runningUnderTnlDev;
127
- }
128
- ```
129
-
130
- The `tnl` value is undefined during builds, previews, and production. During
131
- development, behavior depends on discovery:
132
-
133
- | Development context | Runtime and network behavior |
134
- | --------------------------------------------------- | ----------------------------------------------------------------------------------------- |
135
- | No generated metadata or `tnl dev` environment | `tnl` is undefined; no tunnel configuration |
136
- | Metadata without a matching development socket | Frozen metadata, `runningUnderTnlDev: false`; no tunnel configuration |
137
- | `tnl dev` environment or discovered matching socket | Assigned metadata, `runningUnderTnlDev: true`; configure and register the actual listener |
138
-
139
- You can start `tnl dev web` in one terminal and start the framework from the
140
- service directory in another. The integration looks for the development socket
141
- when the framework configuration loads; it does not keep searching afterward.
142
-
143
- Project metadata is a snapshot. It does not show whether the route or local
144
- service is healthy. The values are read-only, and malformed metadata causes an
145
- error. `tnl publish` cannot add metadata to an application that is already
146
- running.
147
-
148
- ## Next.js
149
-
150
- ```ts
151
- // next.config.ts
152
- import { withTnl } from "@tnldotdev/tnl/next";
153
-
154
- export default withTnl({
155
- reactStrictMode: true,
156
- });
157
- ```
158
-
159
- `withTnl` accepts every Next.js configuration form: an object, a promise, or a
160
- synchronous or asynchronous function. During `tnl dev`, it:
161
-
162
- - Adds the assigned hostname to `allowedDevOrigins`.
163
- - Injects the project runtime.
164
- - Registers the listener after Next.js reports the port it actually used.
165
-
166
- The integration preserves existing host and port settings. If no port was
167
- forced, Next.js can retry when its preferred port is occupied. `tnl` waits for
168
- the port Next.js chooses instead of forwarding to another process. A port set by
169
- `tnl dev --port` must match exactly. Host settings remain independent, so the
170
- development server can still be exposed on the LAN.
171
-
172
- Next.js 16.3.4 or newer is supported.
173
-
174
- ## Vite
175
-
176
- ```ts
177
- // vite.config.ts
178
- import { defineConfig } from "vite";
179
- import tnl from "@tnldotdev/tnl/vite";
180
-
181
- export default defineConfig({
182
- plugins: [tnl()],
183
- });
184
- ```
185
-
186
- During `tnl dev`, the plugin allows the assigned hostname, injects the project
187
- runtime, and registers the listener after Vite chooses its port. Vite keeps its
188
- normal host and port behavior, including retries when a port is occupied. A port
189
- set by `tnl dev --port` must match exactly. Host settings can still expose Vite
190
- on the LAN.
191
-
192
- Without a development socket, the plugin only injects generated project
193
- metadata. It does nothing during builds and previews. Vite 6.0.9 or newer is
194
- supported.
195
-
196
- Next.js and Vite are optional peers, so only the framework already used by the
197
- project is required. Keep hostname, policy, server, service, and command
198
- settings in project configuration rather than passing integration options.
199
-
200
- ### Listener Requirements
201
-
202
- The target must use HTTP over loopback. A wildcard binding (`0.0.0.0` or `::`)
203
- is allowed because tnl can still connect through loopback. Binding only to a
204
- specific LAN address is not supported. Vite middleware mode does not provide a
205
- supported listener. Next.js must report an HTTP listener URL.
206
-
207
- When a port is forced, the integration checks the actual listener instead of
208
- trusting configuration alone. The integration preserves Vite's
209
- `allowedHosts: true` and will not re-enable host filtering.
210
-
211
- Deploy `tnld` with the release container or install it from Homebrew or a
212
- 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
- /** The Git worktree or project directory that contains tnl.config.ts. */
49
- export interface TnlWorktree {
50
- readonly isGit: boolean;
51
- /** DNS-safe worktree label derived from this worktree and client state. */
52
- readonly label: string;
53
- readonly name: string;
54
- readonly root: string;
55
- }
56
-
57
- /** Values passed 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
- /** Adds type checking to a 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
- }