@apps-in-toss/devtools 3.1.0-beta.0 → 3.1.0
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/CHANGELOG.md +34 -0
- package/LICENSE +201 -28
- package/README.md +237 -871
- package/dist/mock/2x.d.ts +3 -155
- package/dist/mock/2x.js +1305 -1559
- package/dist/mock/3x.d.ts +27 -123
- package/dist/mock/3x.js +1275 -1521
- package/dist/mock/index.d.ts +27 -123
- package/dist/mock/index.js +1275 -1521
- package/dist/panel/index.js +154 -106
- package/dist/unplugin/index.cjs +12 -147
- package/dist/unplugin/index.d.cts +408 -78
- package/dist/unplugin/index.d.ts +408 -78
- package/dist/unplugin/index.js +13 -147
- package/package.json +5 -8
- package/dist/tunnel-BvEf1qGV.js +0 -186
- package/dist/tunnel-DtCTOUlp.cjs +0 -187
- package/dist/unplugin/tunnel.cjs +0 -191
- package/dist/unplugin/tunnel.d.cts +0 -140
- package/dist/unplugin/tunnel.d.ts +0 -140
- package/dist/unplugin/tunnel.js +0 -186
package/dist/unplugin/tunnel.cjs
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let node_fs = require("node:fs");
|
|
3
|
-
let node_fs_promises = require("node:fs/promises");
|
|
4
|
-
let node_path = require("node:path");
|
|
5
|
-
//#region src/unplugin/tunnel.ts
|
|
6
|
-
/**
|
|
7
|
-
* Cloudflare quick-tunnel helper for the devtools unplugin.
|
|
8
|
-
*
|
|
9
|
-
* Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is
|
|
10
|
-
* on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common
|
|
11
|
-
* case. This is the one place in `@apps-in-toss/devtools` that depends on Node-only
|
|
12
|
-
* APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of
|
|
13
|
-
* jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the
|
|
14
|
-
* "web 모드는 e2e" rule in CLAUDE.md). The pure helpers below
|
|
15
|
-
* (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.
|
|
16
|
-
*/
|
|
17
|
-
/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */
|
|
18
|
-
const TRYCLOUDFLARE_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
19
|
-
/**
|
|
20
|
-
* Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared
|
|
21
|
-
* output, or `null` if the line doesn't contain one. Pulled out as a pure
|
|
22
|
-
* function so it can be unit-tested without spawning anything.
|
|
23
|
-
*/
|
|
24
|
-
function parseTrycloudflareUrl(line) {
|
|
25
|
-
const m = line.match(TRYCLOUDFLARE_RE);
|
|
26
|
-
return m ? m[0] : null;
|
|
27
|
-
}
|
|
28
|
-
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
29
|
-
/**
|
|
30
|
-
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
31
|
-
* already on the phone's home screen, scanning this opens it directly into the
|
|
32
|
-
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
33
|
-
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
34
|
-
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
35
|
-
* land on a "please install" screen.
|
|
36
|
-
*
|
|
37
|
-
* When `opts.name` is given (non-blank), it is added as `&name=` so the launcher
|
|
38
|
-
* partner bar shows the app name instead of the generic default (#498).
|
|
39
|
-
*
|
|
40
|
-
* When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the
|
|
41
|
-
* launcher enters game nav chrome (floating capsule, no full bar) automatically
|
|
42
|
-
* on scan. `'partner'` is the launcher's implicit default and is not added to
|
|
43
|
-
* keep the URL clean (#584).
|
|
44
|
-
*
|
|
45
|
-
* When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended
|
|
46
|
-
* so the launcher partner bar renders with a transparent background (#587).
|
|
47
|
-
*
|
|
48
|
-
* When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is
|
|
49
|
-
* appended so the launcher partner bar uses the matching foreground colour (#587).
|
|
50
|
-
*/
|
|
51
|
-
function buildLauncherDeepLink(tunnelUrl, optsParam) {
|
|
52
|
-
const opts = optsParam ?? {};
|
|
53
|
-
let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
54
|
-
if (opts.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
|
|
55
|
-
if (opts.webViewType === "game") url += "&navBarType=game";
|
|
56
|
-
if (opts.navBarTransparent === true) url += "&navBarTransparent=1";
|
|
57
|
-
if (opts.navBarTheme === "light" || opts.navBarTheme === "dark") url += `&navBarTheme=${opts.navBarTheme}`;
|
|
58
|
-
return url;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
62
|
-
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
63
|
-
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
64
|
-
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
65
|
-
*/
|
|
66
|
-
async function printTunnelBanner(url, opts = {}) {
|
|
67
|
-
const log = opts.log ?? ((m) => console.log(m));
|
|
68
|
-
const deepLink = buildLauncherDeepLink(url, {
|
|
69
|
-
name: opts.name,
|
|
70
|
-
webViewType: opts.webViewType,
|
|
71
|
-
navBarTransparent: opts.navBarTransparent,
|
|
72
|
-
navBarTheme: opts.navBarTheme
|
|
73
|
-
});
|
|
74
|
-
log([
|
|
75
|
-
"",
|
|
76
|
-
" ┌─ @apps-in-toss/devtools · live tunnel ────────────────────────────",
|
|
77
|
-
` │ ${url}`,
|
|
78
|
-
" │",
|
|
79
|
-
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
80
|
-
" │ Then scan the QR below — it opens the launcher directly",
|
|
81
|
-
" │ into this tunnel URL (no manual paste needed).",
|
|
82
|
-
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
83
|
-
" │ not for production use.",
|
|
84
|
-
" └──────────────────────────────────────────────────────────────",
|
|
85
|
-
""
|
|
86
|
-
].join("\n"));
|
|
87
|
-
if (opts.qr !== false) {
|
|
88
|
-
const qrcode = (await import("qrcode-terminal")).default;
|
|
89
|
-
await new Promise((resolve) => {
|
|
90
|
-
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
91
|
-
log(out);
|
|
92
|
-
resolve();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
99
|
-
*
|
|
100
|
-
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
101
|
-
* that carry those hostnames so tunnel host values never appear in error
|
|
102
|
-
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
103
|
-
*
|
|
104
|
-
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
105
|
-
* placeholder text is emitted.
|
|
106
|
-
*/
|
|
107
|
-
function sanitizeCloudflaredOutput(line) {
|
|
108
|
-
let s = line.replace(/(?:https?|wss?):\/\/[a-z0-9-]+\.trycloudflare\.com(?:\/[^\s]*)*/gi, (m) => m.replace(/[a-z0-9-]+\.trycloudflare\.com/i, "<HOST>.trycloudflare.com"));
|
|
109
|
-
s = s.replace(/[a-z0-9-]+\.trycloudflare\.com/gi, "<HOST>.trycloudflare.com");
|
|
110
|
-
return s;
|
|
111
|
-
}
|
|
112
|
-
const URL_TIMEOUT_MS = 2e4;
|
|
113
|
-
/**
|
|
114
|
-
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
115
|
-
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
116
|
-
* on first use if it is not already installed. Rejects with a friendly error if
|
|
117
|
-
* no URL appears within {@link URL_TIMEOUT_MS}.
|
|
118
|
-
*/
|
|
119
|
-
async function startQuickTunnel(port) {
|
|
120
|
-
const { bin, install, Tunnel } = await import("cloudflared");
|
|
121
|
-
if (!(0, node_fs.existsSync)(bin)) {
|
|
122
|
-
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(bin), { recursive: true });
|
|
123
|
-
await install(bin);
|
|
124
|
-
}
|
|
125
|
-
const tunnel = Tunnel.quick(`http://localhost:${port}`);
|
|
126
|
-
let stopped = false;
|
|
127
|
-
const stop = () => {
|
|
128
|
-
if (stopped) return;
|
|
129
|
-
stopped = true;
|
|
130
|
-
try {
|
|
131
|
-
tunnel.stop();
|
|
132
|
-
} catch {}
|
|
133
|
-
};
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const stderrLines = [];
|
|
136
|
-
/**
|
|
137
|
-
* Format the last `n` sanitized stderr lines as a diagnostic appendix.
|
|
138
|
-
* Returns an empty string when no lines have been collected.
|
|
139
|
-
*/
|
|
140
|
-
const stderrTail = (n = 15) => {
|
|
141
|
-
if (stderrLines.length === 0) return "";
|
|
142
|
-
const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join("");
|
|
143
|
-
return `\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\n${tail}`;
|
|
144
|
-
};
|
|
145
|
-
const timer = setTimeout(() => {
|
|
146
|
-
cleanup();
|
|
147
|
-
stop();
|
|
148
|
-
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.${stderrTail()}`));
|
|
149
|
-
}, URL_TIMEOUT_MS);
|
|
150
|
-
const onUrl = (line) => {
|
|
151
|
-
const found = parseTrycloudflareUrl(line);
|
|
152
|
-
if (!found) return;
|
|
153
|
-
clearTimeout(timer);
|
|
154
|
-
cleanup();
|
|
155
|
-
resolve({
|
|
156
|
-
url: found,
|
|
157
|
-
stop
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
const pushStderr = (line) => {
|
|
161
|
-
stderrLines.push(line);
|
|
162
|
-
};
|
|
163
|
-
const cleanup = () => {
|
|
164
|
-
tunnel.off("stdout", onUrl);
|
|
165
|
-
tunnel.off("stderr", onUrl);
|
|
166
|
-
tunnel.off("stderr", pushStderr);
|
|
167
|
-
};
|
|
168
|
-
tunnel.once("url", onUrl);
|
|
169
|
-
tunnel.on("stdout", onUrl);
|
|
170
|
-
tunnel.on("stderr", onUrl);
|
|
171
|
-
tunnel.on("stderr", pushStderr);
|
|
172
|
-
tunnel.once("error", (err) => {
|
|
173
|
-
clearTimeout(timer);
|
|
174
|
-
cleanup();
|
|
175
|
-
stop();
|
|
176
|
-
reject(err);
|
|
177
|
-
});
|
|
178
|
-
tunnel.once("exit", (code) => {
|
|
179
|
-
if (stopped) return;
|
|
180
|
-
clearTimeout(timer);
|
|
181
|
-
cleanup();
|
|
182
|
-
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.${stderrTail()}`));
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
//#endregion
|
|
187
|
-
exports.buildLauncherDeepLink = buildLauncherDeepLink;
|
|
188
|
-
exports.parseTrycloudflareUrl = parseTrycloudflareUrl;
|
|
189
|
-
exports.printTunnelBanner = printTunnelBanner;
|
|
190
|
-
exports.sanitizeCloudflaredOutput = sanitizeCloudflaredOutput;
|
|
191
|
-
exports.startQuickTunnel = startQuickTunnel;
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
//#region src/unplugin/tunnel.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Cloudflare quick-tunnel helper for the devtools unplugin.
|
|
4
|
-
*
|
|
5
|
-
* Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is
|
|
6
|
-
* on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common
|
|
7
|
-
* case. This is the one place in `@apps-in-toss/devtools` that depends on Node-only
|
|
8
|
-
* APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of
|
|
9
|
-
* jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the
|
|
10
|
-
* "web 모드는 e2e" rule in CLAUDE.md). The pure helpers below
|
|
11
|
-
* (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared
|
|
15
|
-
* output, or `null` if the line doesn't contain one. Pulled out as a pure
|
|
16
|
-
* function so it can be unit-tested without spawning anything.
|
|
17
|
-
*/
|
|
18
|
-
declare function parseTrycloudflareUrl(line: string): string | null;
|
|
19
|
-
interface PrintTunnelBannerOptions {
|
|
20
|
-
/** Print an ASCII QR encoding the tunnel URL (default: true). */
|
|
21
|
-
qr?: boolean;
|
|
22
|
-
/** Sink for the banner text (default: `console.log`). Injected for testing. */
|
|
23
|
-
log?: (msg: string) => void;
|
|
24
|
-
/**
|
|
25
|
-
* Human-readable app name to embed as `name=` in the launcher deep-link (#498).
|
|
26
|
-
* When provided (non-blank), the launcher partner bar shows this name instead of
|
|
27
|
-
* the generic default.
|
|
28
|
-
*/
|
|
29
|
-
name?: string;
|
|
30
|
-
/**
|
|
31
|
-
* The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`
|
|
32
|
-
* so the launcher enters game nav chrome automatically on scan (#584).
|
|
33
|
-
* `'partner'` (the default) is the launcher's implicit default — not added to
|
|
34
|
-
* keep the URL clean.
|
|
35
|
-
*/
|
|
36
|
-
webViewType?: "partner" | "game";
|
|
37
|
-
/**
|
|
38
|
-
* Whether the miniapp's navigationBar has `transparentBackground: true`
|
|
39
|
-
* (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).
|
|
40
|
-
* When `true`, the deep-link carries `&navBarTransparent=1` so the launcher
|
|
41
|
-
* partner bar renders with a transparent background (content shows through).
|
|
42
|
-
* `false` / omitted → not added (URL clean, back-compat).
|
|
43
|
-
*/
|
|
44
|
-
navBarTransparent?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,
|
|
47
|
-
* SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries
|
|
48
|
-
* `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground
|
|
49
|
-
* colour. Omitted / other values → not added (URL clean, back-compat).
|
|
50
|
-
*/
|
|
51
|
-
navBarTheme?: "light" | "dark";
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Options for {@link buildLauncherDeepLink}.
|
|
55
|
-
*/
|
|
56
|
-
interface BuildLauncherDeepLinkOptions {
|
|
57
|
-
/**
|
|
58
|
-
* Human-readable app name shown in the partner nav bar (`name=` param, #498).
|
|
59
|
-
* Blank / whitespace-only values are not added.
|
|
60
|
-
*/
|
|
61
|
-
name?: string;
|
|
62
|
-
/**
|
|
63
|
-
* The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the
|
|
64
|
-
* deep-link so the launcher enters game nav chrome automatically on scan (#584).
|
|
65
|
-
* `'partner'` (the launcher's implicit default) is not added to keep the URL
|
|
66
|
-
* clean.
|
|
67
|
-
*/
|
|
68
|
-
webViewType?: "partner" | "game";
|
|
69
|
-
/**
|
|
70
|
-
* Whether the miniapp's navigationBar has `transparentBackground: true`
|
|
71
|
-
* (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).
|
|
72
|
-
* When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher
|
|
73
|
-
* partner bar renders with a transparent background. Omitted when `false` /
|
|
74
|
-
* undefined to keep the URL clean (back-compat).
|
|
75
|
-
*/
|
|
76
|
-
navBarTransparent?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
* The miniapp's navigationBar theme (granite.config `navigationBar.theme`,
|
|
79
|
-
* SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to
|
|
80
|
-
* the deep-link so the launcher partner bar uses the matching foreground colour.
|
|
81
|
-
* Omitted when undefined / other values to keep the URL clean (back-compat).
|
|
82
|
-
*/
|
|
83
|
-
navBarTheme?: "light" | "dark";
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
87
|
-
* already on the phone's home screen, scanning this opens it directly into the
|
|
88
|
-
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
89
|
-
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
90
|
-
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
91
|
-
* land on a "please install" screen.
|
|
92
|
-
*
|
|
93
|
-
* When `opts.name` is given (non-blank), it is added as `&name=` so the launcher
|
|
94
|
-
* partner bar shows the app name instead of the generic default (#498).
|
|
95
|
-
*
|
|
96
|
-
* When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the
|
|
97
|
-
* launcher enters game nav chrome (floating capsule, no full bar) automatically
|
|
98
|
-
* on scan. `'partner'` is the launcher's implicit default and is not added to
|
|
99
|
-
* keep the URL clean (#584).
|
|
100
|
-
*
|
|
101
|
-
* When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended
|
|
102
|
-
* so the launcher partner bar renders with a transparent background (#587).
|
|
103
|
-
*
|
|
104
|
-
* When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is
|
|
105
|
-
* appended so the launcher partner bar uses the matching foreground colour (#587).
|
|
106
|
-
*/
|
|
107
|
-
declare function buildLauncherDeepLink(tunnelUrl: string, optsParam?: BuildLauncherDeepLinkOptions): string;
|
|
108
|
-
/**
|
|
109
|
-
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
110
|
-
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
111
|
-
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
112
|
-
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
113
|
-
*/
|
|
114
|
-
declare function printTunnelBanner(url: string, opts?: PrintTunnelBannerOptions): Promise<void>;
|
|
115
|
-
interface QuickTunnel {
|
|
116
|
-
/** The public `https://*.trycloudflare.com` URL. */
|
|
117
|
-
url: string;
|
|
118
|
-
/** Stop the underlying `cloudflared` process. Idempotent. */
|
|
119
|
-
stop: () => void;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
123
|
-
*
|
|
124
|
-
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
125
|
-
* that carry those hostnames so tunnel host values never appear in error
|
|
126
|
-
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
127
|
-
*
|
|
128
|
-
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
129
|
-
* placeholder text is emitted.
|
|
130
|
-
*/
|
|
131
|
-
declare function sanitizeCloudflaredOutput(line: string): string;
|
|
132
|
-
/**
|
|
133
|
-
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
134
|
-
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
135
|
-
* on first use if it is not already installed. Rejects with a friendly error if
|
|
136
|
-
* no URL appears within {@link URL_TIMEOUT_MS}.
|
|
137
|
-
*/
|
|
138
|
-
declare function startQuickTunnel(port: number): Promise<QuickTunnel>;
|
|
139
|
-
//#endregion
|
|
140
|
-
export { BuildLauncherDeepLinkOptions, PrintTunnelBannerOptions, QuickTunnel, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, sanitizeCloudflaredOutput, startQuickTunnel };
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
//#region src/unplugin/tunnel.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Cloudflare quick-tunnel helper for the devtools unplugin.
|
|
4
|
-
*
|
|
5
|
-
* Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is
|
|
6
|
-
* on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common
|
|
7
|
-
* case. This is the one place in `@apps-in-toss/devtools` that depends on Node-only
|
|
8
|
-
* APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of
|
|
9
|
-
* jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the
|
|
10
|
-
* "web 모드는 e2e" rule in CLAUDE.md). The pure helpers below
|
|
11
|
-
* (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared
|
|
15
|
-
* output, or `null` if the line doesn't contain one. Pulled out as a pure
|
|
16
|
-
* function so it can be unit-tested without spawning anything.
|
|
17
|
-
*/
|
|
18
|
-
declare function parseTrycloudflareUrl(line: string): string | null;
|
|
19
|
-
interface PrintTunnelBannerOptions {
|
|
20
|
-
/** Print an ASCII QR encoding the tunnel URL (default: true). */
|
|
21
|
-
qr?: boolean;
|
|
22
|
-
/** Sink for the banner text (default: `console.log`). Injected for testing. */
|
|
23
|
-
log?: (msg: string) => void;
|
|
24
|
-
/**
|
|
25
|
-
* Human-readable app name to embed as `name=` in the launcher deep-link (#498).
|
|
26
|
-
* When provided (non-blank), the launcher partner bar shows this name instead of
|
|
27
|
-
* the generic default.
|
|
28
|
-
*/
|
|
29
|
-
name?: string;
|
|
30
|
-
/**
|
|
31
|
-
* The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`
|
|
32
|
-
* so the launcher enters game nav chrome automatically on scan (#584).
|
|
33
|
-
* `'partner'` (the default) is the launcher's implicit default — not added to
|
|
34
|
-
* keep the URL clean.
|
|
35
|
-
*/
|
|
36
|
-
webViewType?: "partner" | "game";
|
|
37
|
-
/**
|
|
38
|
-
* Whether the miniapp's navigationBar has `transparentBackground: true`
|
|
39
|
-
* (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).
|
|
40
|
-
* When `true`, the deep-link carries `&navBarTransparent=1` so the launcher
|
|
41
|
-
* partner bar renders with a transparent background (content shows through).
|
|
42
|
-
* `false` / omitted → not added (URL clean, back-compat).
|
|
43
|
-
*/
|
|
44
|
-
navBarTransparent?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,
|
|
47
|
-
* SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries
|
|
48
|
-
* `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground
|
|
49
|
-
* colour. Omitted / other values → not added (URL clean, back-compat).
|
|
50
|
-
*/
|
|
51
|
-
navBarTheme?: "light" | "dark";
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Options for {@link buildLauncherDeepLink}.
|
|
55
|
-
*/
|
|
56
|
-
interface BuildLauncherDeepLinkOptions {
|
|
57
|
-
/**
|
|
58
|
-
* Human-readable app name shown in the partner nav bar (`name=` param, #498).
|
|
59
|
-
* Blank / whitespace-only values are not added.
|
|
60
|
-
*/
|
|
61
|
-
name?: string;
|
|
62
|
-
/**
|
|
63
|
-
* The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the
|
|
64
|
-
* deep-link so the launcher enters game nav chrome automatically on scan (#584).
|
|
65
|
-
* `'partner'` (the launcher's implicit default) is not added to keep the URL
|
|
66
|
-
* clean.
|
|
67
|
-
*/
|
|
68
|
-
webViewType?: "partner" | "game";
|
|
69
|
-
/**
|
|
70
|
-
* Whether the miniapp's navigationBar has `transparentBackground: true`
|
|
71
|
-
* (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).
|
|
72
|
-
* When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher
|
|
73
|
-
* partner bar renders with a transparent background. Omitted when `false` /
|
|
74
|
-
* undefined to keep the URL clean (back-compat).
|
|
75
|
-
*/
|
|
76
|
-
navBarTransparent?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
* The miniapp's navigationBar theme (granite.config `navigationBar.theme`,
|
|
79
|
-
* SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to
|
|
80
|
-
* the deep-link so the launcher partner bar uses the matching foreground colour.
|
|
81
|
-
* Omitted when undefined / other values to keep the URL clean (back-compat).
|
|
82
|
-
*/
|
|
83
|
-
navBarTheme?: "light" | "dark";
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
87
|
-
* already on the phone's home screen, scanning this opens it directly into the
|
|
88
|
-
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
89
|
-
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
90
|
-
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
91
|
-
* land on a "please install" screen.
|
|
92
|
-
*
|
|
93
|
-
* When `opts.name` is given (non-blank), it is added as `&name=` so the launcher
|
|
94
|
-
* partner bar shows the app name instead of the generic default (#498).
|
|
95
|
-
*
|
|
96
|
-
* When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the
|
|
97
|
-
* launcher enters game nav chrome (floating capsule, no full bar) automatically
|
|
98
|
-
* on scan. `'partner'` is the launcher's implicit default and is not added to
|
|
99
|
-
* keep the URL clean (#584).
|
|
100
|
-
*
|
|
101
|
-
* When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended
|
|
102
|
-
* so the launcher partner bar renders with a transparent background (#587).
|
|
103
|
-
*
|
|
104
|
-
* When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is
|
|
105
|
-
* appended so the launcher partner bar uses the matching foreground colour (#587).
|
|
106
|
-
*/
|
|
107
|
-
declare function buildLauncherDeepLink(tunnelUrl: string, optsParam?: BuildLauncherDeepLinkOptions): string;
|
|
108
|
-
/**
|
|
109
|
-
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
110
|
-
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
111
|
-
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
112
|
-
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
113
|
-
*/
|
|
114
|
-
declare function printTunnelBanner(url: string, opts?: PrintTunnelBannerOptions): Promise<void>;
|
|
115
|
-
interface QuickTunnel {
|
|
116
|
-
/** The public `https://*.trycloudflare.com` URL. */
|
|
117
|
-
url: string;
|
|
118
|
-
/** Stop the underlying `cloudflared` process. Idempotent. */
|
|
119
|
-
stop: () => void;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
123
|
-
*
|
|
124
|
-
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
125
|
-
* that carry those hostnames so tunnel host values never appear in error
|
|
126
|
-
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
127
|
-
*
|
|
128
|
-
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
129
|
-
* placeholder text is emitted.
|
|
130
|
-
*/
|
|
131
|
-
declare function sanitizeCloudflaredOutput(line: string): string;
|
|
132
|
-
/**
|
|
133
|
-
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
134
|
-
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
135
|
-
* on first use if it is not already installed. Rejects with a friendly error if
|
|
136
|
-
* no URL appears within {@link URL_TIMEOUT_MS}.
|
|
137
|
-
*/
|
|
138
|
-
declare function startQuickTunnel(port: number): Promise<QuickTunnel>;
|
|
139
|
-
//#endregion
|
|
140
|
-
export { BuildLauncherDeepLinkOptions, PrintTunnelBannerOptions, QuickTunnel, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, sanitizeCloudflaredOutput, startQuickTunnel };
|
package/dist/unplugin/tunnel.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
//#region src/unplugin/tunnel.ts
|
|
5
|
-
/**
|
|
6
|
-
* Cloudflare quick-tunnel helper for the devtools unplugin.
|
|
7
|
-
*
|
|
8
|
-
* Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is
|
|
9
|
-
* on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common
|
|
10
|
-
* case. This is the one place in `@apps-in-toss/devtools` that depends on Node-only
|
|
11
|
-
* APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of
|
|
12
|
-
* jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the
|
|
13
|
-
* "web 모드는 e2e" rule in CLAUDE.md). The pure helpers below
|
|
14
|
-
* (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.
|
|
15
|
-
*/
|
|
16
|
-
/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */
|
|
17
|
-
const TRYCLOUDFLARE_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
18
|
-
/**
|
|
19
|
-
* Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared
|
|
20
|
-
* output, or `null` if the line doesn't contain one. Pulled out as a pure
|
|
21
|
-
* function so it can be unit-tested without spawning anything.
|
|
22
|
-
*/
|
|
23
|
-
function parseTrycloudflareUrl(line) {
|
|
24
|
-
const m = line.match(TRYCLOUDFLARE_RE);
|
|
25
|
-
return m ? m[0] : null;
|
|
26
|
-
}
|
|
27
|
-
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
28
|
-
/**
|
|
29
|
-
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
30
|
-
* already on the phone's home screen, scanning this opens it directly into the
|
|
31
|
-
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
32
|
-
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
33
|
-
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
34
|
-
* land on a "please install" screen.
|
|
35
|
-
*
|
|
36
|
-
* When `opts.name` is given (non-blank), it is added as `&name=` so the launcher
|
|
37
|
-
* partner bar shows the app name instead of the generic default (#498).
|
|
38
|
-
*
|
|
39
|
-
* When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the
|
|
40
|
-
* launcher enters game nav chrome (floating capsule, no full bar) automatically
|
|
41
|
-
* on scan. `'partner'` is the launcher's implicit default and is not added to
|
|
42
|
-
* keep the URL clean (#584).
|
|
43
|
-
*
|
|
44
|
-
* When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended
|
|
45
|
-
* so the launcher partner bar renders with a transparent background (#587).
|
|
46
|
-
*
|
|
47
|
-
* When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is
|
|
48
|
-
* appended so the launcher partner bar uses the matching foreground colour (#587).
|
|
49
|
-
*/
|
|
50
|
-
function buildLauncherDeepLink(tunnelUrl, optsParam) {
|
|
51
|
-
const opts = optsParam ?? {};
|
|
52
|
-
let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
53
|
-
if (opts.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
|
|
54
|
-
if (opts.webViewType === "game") url += "&navBarType=game";
|
|
55
|
-
if (opts.navBarTransparent === true) url += "&navBarTransparent=1";
|
|
56
|
-
if (opts.navBarTheme === "light" || opts.navBarTheme === "dark") url += `&navBarTheme=${opts.navBarTheme}`;
|
|
57
|
-
return url;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
61
|
-
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
62
|
-
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
63
|
-
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
64
|
-
*/
|
|
65
|
-
async function printTunnelBanner(url, opts = {}) {
|
|
66
|
-
const log = opts.log ?? ((m) => console.log(m));
|
|
67
|
-
const deepLink = buildLauncherDeepLink(url, {
|
|
68
|
-
name: opts.name,
|
|
69
|
-
webViewType: opts.webViewType,
|
|
70
|
-
navBarTransparent: opts.navBarTransparent,
|
|
71
|
-
navBarTheme: opts.navBarTheme
|
|
72
|
-
});
|
|
73
|
-
log([
|
|
74
|
-
"",
|
|
75
|
-
" ┌─ @apps-in-toss/devtools · live tunnel ────────────────────────────",
|
|
76
|
-
` │ ${url}`,
|
|
77
|
-
" │",
|
|
78
|
-
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
79
|
-
" │ Then scan the QR below — it opens the launcher directly",
|
|
80
|
-
" │ into this tunnel URL (no manual paste needed).",
|
|
81
|
-
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
82
|
-
" │ not for production use.",
|
|
83
|
-
" └──────────────────────────────────────────────────────────────",
|
|
84
|
-
""
|
|
85
|
-
].join("\n"));
|
|
86
|
-
if (opts.qr !== false) {
|
|
87
|
-
const qrcode = (await import("qrcode-terminal")).default;
|
|
88
|
-
await new Promise((resolve) => {
|
|
89
|
-
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
90
|
-
log(out);
|
|
91
|
-
resolve();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
98
|
-
*
|
|
99
|
-
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
100
|
-
* that carry those hostnames so tunnel host values never appear in error
|
|
101
|
-
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
102
|
-
*
|
|
103
|
-
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
104
|
-
* placeholder text is emitted.
|
|
105
|
-
*/
|
|
106
|
-
function sanitizeCloudflaredOutput(line) {
|
|
107
|
-
let s = line.replace(/(?:https?|wss?):\/\/[a-z0-9-]+\.trycloudflare\.com(?:\/[^\s]*)*/gi, (m) => m.replace(/[a-z0-9-]+\.trycloudflare\.com/i, "<HOST>.trycloudflare.com"));
|
|
108
|
-
s = s.replace(/[a-z0-9-]+\.trycloudflare\.com/gi, "<HOST>.trycloudflare.com");
|
|
109
|
-
return s;
|
|
110
|
-
}
|
|
111
|
-
const URL_TIMEOUT_MS = 2e4;
|
|
112
|
-
/**
|
|
113
|
-
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
114
|
-
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
115
|
-
* on first use if it is not already installed. Rejects with a friendly error if
|
|
116
|
-
* no URL appears within {@link URL_TIMEOUT_MS}.
|
|
117
|
-
*/
|
|
118
|
-
async function startQuickTunnel(port) {
|
|
119
|
-
const { bin, install, Tunnel } = await import("cloudflared");
|
|
120
|
-
if (!existsSync(bin)) {
|
|
121
|
-
await mkdir(dirname(bin), { recursive: true });
|
|
122
|
-
await install(bin);
|
|
123
|
-
}
|
|
124
|
-
const tunnel = Tunnel.quick(`http://localhost:${port}`);
|
|
125
|
-
let stopped = false;
|
|
126
|
-
const stop = () => {
|
|
127
|
-
if (stopped) return;
|
|
128
|
-
stopped = true;
|
|
129
|
-
try {
|
|
130
|
-
tunnel.stop();
|
|
131
|
-
} catch {}
|
|
132
|
-
};
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
const stderrLines = [];
|
|
135
|
-
/**
|
|
136
|
-
* Format the last `n` sanitized stderr lines as a diagnostic appendix.
|
|
137
|
-
* Returns an empty string when no lines have been collected.
|
|
138
|
-
*/
|
|
139
|
-
const stderrTail = (n = 15) => {
|
|
140
|
-
if (stderrLines.length === 0) return "";
|
|
141
|
-
const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join("");
|
|
142
|
-
return `\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\n${tail}`;
|
|
143
|
-
};
|
|
144
|
-
const timer = setTimeout(() => {
|
|
145
|
-
cleanup();
|
|
146
|
-
stop();
|
|
147
|
-
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.${stderrTail()}`));
|
|
148
|
-
}, URL_TIMEOUT_MS);
|
|
149
|
-
const onUrl = (line) => {
|
|
150
|
-
const found = parseTrycloudflareUrl(line);
|
|
151
|
-
if (!found) return;
|
|
152
|
-
clearTimeout(timer);
|
|
153
|
-
cleanup();
|
|
154
|
-
resolve({
|
|
155
|
-
url: found,
|
|
156
|
-
stop
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
const pushStderr = (line) => {
|
|
160
|
-
stderrLines.push(line);
|
|
161
|
-
};
|
|
162
|
-
const cleanup = () => {
|
|
163
|
-
tunnel.off("stdout", onUrl);
|
|
164
|
-
tunnel.off("stderr", onUrl);
|
|
165
|
-
tunnel.off("stderr", pushStderr);
|
|
166
|
-
};
|
|
167
|
-
tunnel.once("url", onUrl);
|
|
168
|
-
tunnel.on("stdout", onUrl);
|
|
169
|
-
tunnel.on("stderr", onUrl);
|
|
170
|
-
tunnel.on("stderr", pushStderr);
|
|
171
|
-
tunnel.once("error", (err) => {
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
cleanup();
|
|
174
|
-
stop();
|
|
175
|
-
reject(err);
|
|
176
|
-
});
|
|
177
|
-
tunnel.once("exit", (code) => {
|
|
178
|
-
if (stopped) return;
|
|
179
|
-
clearTimeout(timer);
|
|
180
|
-
cleanup();
|
|
181
|
-
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.${stderrTail()}`));
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
//#endregion
|
|
186
|
-
export { buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, sanitizeCloudflaredOutput, startQuickTunnel };
|