@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.
- package/dist/bin/tnl.d.ts +2 -0
- package/dist/bin/tnl.js +15 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.gen.d.ts +133 -0
- package/dist/config.gen.js +2 -0
- package/dist/config.js +4 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/internal/dev.d.ts +1 -1
- package/dist/internal/dev.js +10 -7
- package/dist/internal/launcher.d.ts +8 -0
- package/dist/internal/launcher.js +55 -0
- package/dist/internal/native-targets.d.ts +9 -0
- package/{lib/native-targets.mjs → dist/internal/native-targets.js} +4 -8
- package/dist/internal/runtime.d.ts +3 -3
- package/dist/internal/runtime.js +5 -4
- package/dist/next.d.ts +1 -1
- package/dist/next.js +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/package.json +32 -33
- package/readme.md +177 -0
- package/README.md +0 -196
- package/bin/tnl.mjs +0 -13
- package/lib/config.d.ts +0 -69
- package/lib/config.mjs +0 -3
- package/lib/launcher.mjs +0 -65
package/dist/bin/tnl.js
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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;
|
package/dist/config.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type ProjectMetadata } from "./internal/runtime.js";
|
|
2
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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);
|
package/dist/internal/dev.d.ts
CHANGED
|
@@ -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
|
|
40
|
+
/** Parses a decimal listener port from 1 through 65535. */
|
|
41
41
|
export declare function parseListenerPort(value: string, source: string): number;
|
package/dist/internal/dev.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
13
|
+
/** Checks browser-safe project metadata and makes it read-only. */
|
|
14
14
|
export declare function parseProjectMetadata(value: unknown, description: string): ProjectMetadata;
|
|
15
|
-
/**
|
|
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;
|
package/dist/internal/runtime.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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": "./
|
|
31
|
-
"import": "./
|
|
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.
|
|
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.
|
|
54
|
-
"react": "19.
|
|
55
|
-
"react-dom": "19.
|
|
56
|
-
"vite": "8.
|
|
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.
|
|
75
|
-
"@tnldotdev/tnl-darwin-x64": "0.1.0-rc.
|
|
76
|
-
"@tnldotdev/tnl-linux-arm64": "0.1.0-rc.
|
|
77
|
-
"@tnldotdev/tnl-linux-x64": "0.1.0-rc.
|
|
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": "
|
|
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
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
|
-
}
|