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