buncargo 3.0.0 → 3.2.3
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/cli/bin.js +10 -8
- package/dist/cli/index.js +2 -2
- package/dist/cli/run-cli.d.ts +10 -2
- package/dist/core/quick-tunnel/cloudflared-process.d.ts +10 -0
- package/dist/core/quick-tunnel/constants.d.ts +9 -0
- package/dist/core/quick-tunnel/index.d.ts +17 -0
- package/dist/core/quick-tunnel/install.d.ts +1 -0
- package/dist/core/tunnel.d.ts +3 -2
- package/dist/environment/index.js +2 -2
- package/dist/environment/logging.d.ts +6 -6
- package/dist/environment/only-apps.d.ts +10 -0
- package/dist/index-3eyrdxw9.js +577 -0
- package/dist/index-5aq985p4.js +250 -0
- package/dist/index-6cmex7m5.js +72 -0
- package/dist/index-6d6x175r.js +572 -0
- package/dist/index-7v19es2e.js +666 -0
- package/dist/index-9wyhzw0h.js +574 -0
- package/dist/index-ag90ry8t.js +576 -0
- package/dist/index-bycj26kj.js +72 -0
- package/dist/index-byeqyjrz.js +72 -0
- package/dist/index-enj4zdma.js +574 -0
- package/dist/index-k370bech.js +72 -0
- package/dist/index-mf4vjhm3.js +362 -0
- package/dist/index-n5g93an7.js +250 -0
- package/dist/index-n6z0qw70.js +666 -0
- package/dist/index-qa8akv6y.js +666 -0
- package/dist/index-vg55rq0y.js +250 -0
- package/dist/index-vs81yaks.js +244 -0
- package/dist/index-x54nbgs7.js +355 -0
- package/dist/index-yz4jfz7z.js +338 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -8
- package/dist/loader/index.js +3 -3
- package/dist/types/all-types.d.ts +46 -3
- package/package.json +147 -145
- package/readme.md +16 -0
- package/src/cli/run-cli.ts +27 -12
- package/src/core/quick-tunnel/cloudflared-process.ts +83 -0
- package/src/core/quick-tunnel/constants.ts +31 -0
- package/src/core/quick-tunnel/index.ts +96 -0
- package/src/core/quick-tunnel/install.ts +160 -0
- package/src/core/tunnel.ts +42 -16
- package/src/environment/create-dev-environment.ts +123 -13
- package/src/environment/logging.ts +34 -20
- package/src/environment/only-apps.ts +34 -0
- package/src/index.ts +3 -0
- package/src/types/all-types.ts +56 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths and release metadata for the cloudflared binary.
|
|
3
|
+
* Derived from unjs/untun (MIT), originally forked from node-cloudflared.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
export const CLOUDFLARED_VERSION =
|
|
10
|
+
process.env.CLOUDFLARED_VERSION || "2023.10.0";
|
|
11
|
+
|
|
12
|
+
export const RELEASE_BASE =
|
|
13
|
+
"https://github.com/cloudflare/cloudflared/releases/";
|
|
14
|
+
|
|
15
|
+
/** Directory for buncargo-managed cloudflared (avoid clashing with untun's node-untun). */
|
|
16
|
+
export const cloudflaredBinPath = path.join(
|
|
17
|
+
tmpdir(),
|
|
18
|
+
"buncargo-cloudflared",
|
|
19
|
+
process.platform === "win32"
|
|
20
|
+
? `cloudflared.${CLOUDFLARED_VERSION}.exe`
|
|
21
|
+
: `cloudflared.${CLOUDFLARED_VERSION}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const cloudflaredNotice = `
|
|
25
|
+
🔥 Your installation of cloudflared software constitutes a symbol of your signature
|
|
26
|
+
indicating that you accept the terms of the Cloudflare License, Terms and Privacy Policy.
|
|
27
|
+
|
|
28
|
+
❯ License: \`https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/license/\`
|
|
29
|
+
❯ Terms: \`https://www.cloudflare.com/terms/\`
|
|
30
|
+
❯ Privacy Policy: \`https://www.cloudflare.com/privacypolicy/\`
|
|
31
|
+
`;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Quick Tunnel via the cloudflared CLI (same approach as unjs/untun).
|
|
3
|
+
* License / download flow adapted from unjs/untun (MIT).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { startCloudflaredTunnel } from "./cloudflared-process";
|
|
8
|
+
import { cloudflaredBinPath, cloudflaredNotice } from "./constants";
|
|
9
|
+
import { installCloudflared } from "./install";
|
|
10
|
+
|
|
11
|
+
export interface QuickTunnelOptions {
|
|
12
|
+
url?: string;
|
|
13
|
+
port?: number | string;
|
|
14
|
+
hostname?: string;
|
|
15
|
+
protocol?: "http" | "https";
|
|
16
|
+
verifyTLS?: boolean;
|
|
17
|
+
acceptCloudflareNotice?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QuickTunnel {
|
|
21
|
+
getURL: () => Promise<string>;
|
|
22
|
+
close: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolvedLocalUrl(opts: QuickTunnelOptions): string {
|
|
26
|
+
return (
|
|
27
|
+
opts.url ??
|
|
28
|
+
`${opts.protocol || "http"}://${opts.hostname ?? "localhost"}:${opts.port ?? 3000}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envAcceptsCloudflareNotice(): boolean {
|
|
33
|
+
const v = process.env.BUNCARGO_ACCEPT_CLOUDFLARE_NOTICE;
|
|
34
|
+
const u = process.env.UNTUN_ACCEPT_CLOUDFLARE_NOTICE;
|
|
35
|
+
return v === "1" || v === "true" || u === "1" || u === "true";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function promptInstallCloudflared(): Promise<boolean> {
|
|
39
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const rl = createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
rl.question(
|
|
48
|
+
"Do you agree with the above terms and wish to install the binary from GitHub? (y/N) ",
|
|
49
|
+
(answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start a Cloudflare quick tunnel to a local HTTP(S) URL.
|
|
59
|
+
* Returns undefined if the user declines the cloudflared install (when binary is missing).
|
|
60
|
+
*/
|
|
61
|
+
export async function startQuickTunnel(
|
|
62
|
+
opts: QuickTunnelOptions,
|
|
63
|
+
): Promise<QuickTunnel | undefined> {
|
|
64
|
+
const url = resolvedLocalUrl(opts);
|
|
65
|
+
|
|
66
|
+
console.log(`Starting cloudflared tunnel to ${url}`);
|
|
67
|
+
|
|
68
|
+
if (!existsSync(cloudflaredBinPath)) {
|
|
69
|
+
console.log(cloudflaredNotice);
|
|
70
|
+
const canInstall =
|
|
71
|
+
opts.acceptCloudflareNotice ||
|
|
72
|
+
envAcceptsCloudflareNotice() ||
|
|
73
|
+
(await promptInstallCloudflared());
|
|
74
|
+
if (!canInstall) {
|
|
75
|
+
console.error("Skipping tunnel setup.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await installCloudflared();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cfArgs: Record<string, string | number | null> = { "--url": url };
|
|
82
|
+
// Boolean flag: use `null` value so spawn does not pass a stray empty argv (see cloudflared-process).
|
|
83
|
+
if (!opts.verifyTLS) {
|
|
84
|
+
cfArgs["--no-tls-verify"] = null;
|
|
85
|
+
}
|
|
86
|
+
const tunnel = startCloudflaredTunnel(cfArgs);
|
|
87
|
+
|
|
88
|
+
const cleanup = async () => {
|
|
89
|
+
tunnel.stop();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getURL: async () => await tunnel.url,
|
|
94
|
+
close: cleanup,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download cloudflared from GitHub releases.
|
|
3
|
+
* Derived from unjs/untun (MIT), originally forked from node-cloudflared.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
CLOUDFLARED_VERSION,
|
|
11
|
+
cloudflaredBinPath,
|
|
12
|
+
RELEASE_BASE,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
|
|
15
|
+
const LINUX_URL: Partial<Record<NodeJS.Architecture, string>> = {
|
|
16
|
+
arm64: "cloudflared-linux-arm64",
|
|
17
|
+
arm: "cloudflared-linux-arm",
|
|
18
|
+
x64: "cloudflared-linux-amd64",
|
|
19
|
+
ia32: "cloudflared-linux-386",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MACOS_URL: Partial<Record<NodeJS.Architecture, string>> = {
|
|
23
|
+
arm64: "cloudflared-darwin-amd64.tgz",
|
|
24
|
+
x64: "cloudflared-darwin-amd64.tgz",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const WINDOWS_URL: Partial<Record<NodeJS.Architecture, string>> = {
|
|
28
|
+
x64: "cloudflared-windows-amd64.exe",
|
|
29
|
+
ia32: "cloudflared-windows-386.exe",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function resolveBase(version: string): string {
|
|
33
|
+
if (version === "latest") {
|
|
34
|
+
return `${RELEASE_BASE}latest/download/`;
|
|
35
|
+
}
|
|
36
|
+
return `${RELEASE_BASE}download/${version}/`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function installCloudflared(
|
|
40
|
+
to: string = cloudflaredBinPath,
|
|
41
|
+
version = CLOUDFLARED_VERSION,
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
switch (process.platform) {
|
|
44
|
+
case "linux": {
|
|
45
|
+
return installLinux(to, version);
|
|
46
|
+
}
|
|
47
|
+
case "darwin": {
|
|
48
|
+
return installMacos(to, version);
|
|
49
|
+
}
|
|
50
|
+
case "win32": {
|
|
51
|
+
return installWindows(to, version);
|
|
52
|
+
}
|
|
53
|
+
default: {
|
|
54
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function installLinux(
|
|
60
|
+
to: string,
|
|
61
|
+
version = CLOUDFLARED_VERSION,
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
const file = LINUX_URL[process.arch];
|
|
64
|
+
|
|
65
|
+
if (file === undefined) {
|
|
66
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await download(resolveBase(version) + file, to);
|
|
70
|
+
fs.chmodSync(to, 0o755);
|
|
71
|
+
return to;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function installMacos(
|
|
75
|
+
to: string,
|
|
76
|
+
version = CLOUDFLARED_VERSION,
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const file = MACOS_URL[process.arch];
|
|
79
|
+
|
|
80
|
+
if (file === undefined) {
|
|
81
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await download(resolveBase(version) + file, `${to}.tgz`);
|
|
85
|
+
if (process.env.DEBUG) {
|
|
86
|
+
console.log(`Extracting to ${to}`);
|
|
87
|
+
}
|
|
88
|
+
execSync(`tar -xzf ${path.basename(`${to}.tgz`)}`, {
|
|
89
|
+
cwd: path.dirname(to),
|
|
90
|
+
});
|
|
91
|
+
fs.unlinkSync(`${to}.tgz`);
|
|
92
|
+
fs.renameSync(`${path.dirname(to)}/cloudflared`, to);
|
|
93
|
+
return to;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function installWindows(
|
|
97
|
+
to: string,
|
|
98
|
+
version = CLOUDFLARED_VERSION,
|
|
99
|
+
): Promise<string> {
|
|
100
|
+
const file = WINDOWS_URL[process.arch];
|
|
101
|
+
|
|
102
|
+
if (file === undefined) {
|
|
103
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await download(resolveBase(version) + file, to);
|
|
107
|
+
return to;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function download(url: string, to: string, redirect = 0): Promise<string> {
|
|
111
|
+
if (redirect === 0) {
|
|
112
|
+
if (process.env.DEBUG) {
|
|
113
|
+
console.log(`Downloading ${url} to ${to}`);
|
|
114
|
+
}
|
|
115
|
+
} else if (process.env.DEBUG) {
|
|
116
|
+
console.log(`Redirecting to ${url}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
if (!fs.existsSync(path.dirname(to))) {
|
|
121
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let done = true;
|
|
125
|
+
const file = fs.createWriteStream(to);
|
|
126
|
+
const request = https.get(url, (res) => {
|
|
127
|
+
if (res.statusCode === 302 && res.headers.location !== undefined) {
|
|
128
|
+
const redirection = res.headers.location;
|
|
129
|
+
done = false;
|
|
130
|
+
file.close(() => {
|
|
131
|
+
void download(redirection, to, redirect + 1).then(resolve, reject);
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
res.pipe(file);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
file.on("finish", () => {
|
|
139
|
+
if (done) {
|
|
140
|
+
file.close(() => {
|
|
141
|
+
resolve(to);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
request.on("error", (err) => {
|
|
147
|
+
fs.unlink(to, () => {
|
|
148
|
+
reject(err);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
file.on("error", (err) => {
|
|
153
|
+
fs.unlink(to, () => {
|
|
154
|
+
reject(err);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
request.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
package/src/core/tunnel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { startTunnel } from "untun";
|
|
2
1
|
import type { AppConfig, DevEnvironment, ServiceConfig } from "../types";
|
|
2
|
+
import { startQuickTunnel } from "./quick-tunnel";
|
|
3
3
|
|
|
4
4
|
export interface PublicExposeTarget {
|
|
5
5
|
kind: "service" | "app";
|
|
@@ -15,7 +15,8 @@ export interface PublicTunnel {
|
|
|
15
15
|
close: () => Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
interface
|
|
18
|
+
interface TunnelBackendResult {
|
|
19
|
+
getURL?: () => Promise<string>;
|
|
19
20
|
url?: string;
|
|
20
21
|
publicUrl?: string;
|
|
21
22
|
tunnelUrl?: string;
|
|
@@ -33,11 +34,17 @@ function parseExposeNames(exposeValue?: string): Set<string> | null {
|
|
|
33
34
|
return new Set(names);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
/** Resolves public origin from tunnel backends (sync fields or untun-style async getURL). */
|
|
38
|
+
async function resolvePublicUrl(
|
|
39
|
+
tunnel: TunnelBackendResult,
|
|
40
|
+
): Promise<string | null> {
|
|
41
|
+
if (typeof tunnel.getURL === "function") {
|
|
42
|
+
return await tunnel.getURL();
|
|
43
|
+
}
|
|
37
44
|
return tunnel.url ?? tunnel.publicUrl ?? tunnel.tunnelUrl ?? null;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function toCloseFn(tunnel:
|
|
47
|
+
function toCloseFn(tunnel: TunnelBackendResult): () => Promise<void> {
|
|
41
48
|
const close = tunnel.close ?? tunnel.stop ?? tunnel.destroy;
|
|
42
49
|
if (!close) return async () => {};
|
|
43
50
|
return async () => {
|
|
@@ -111,37 +118,56 @@ export function resolveExposeTargets<
|
|
|
111
118
|
export async function startPublicTunnels(
|
|
112
119
|
targets: PublicExposeTarget[],
|
|
113
120
|
options: {
|
|
114
|
-
start?: (input: {
|
|
121
|
+
start?: (input: {
|
|
122
|
+
url: string;
|
|
123
|
+
}) => Promise<TunnelBackendResult | undefined>;
|
|
115
124
|
} = {},
|
|
116
125
|
): Promise<PublicTunnel[]> {
|
|
117
|
-
const start = options.start ?? ((input) =>
|
|
118
|
-
const tunnels: PublicTunnel[] = [];
|
|
126
|
+
const start = options.start ?? ((input) => startQuickTunnel(input));
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
const settled = await Promise.allSettled(
|
|
129
|
+
targets.map(async (target) => {
|
|
122
130
|
const localUrl = `http://localhost:${target.port}`;
|
|
123
131
|
const tunnel = (await start({
|
|
124
132
|
url: localUrl,
|
|
125
|
-
})) as
|
|
126
|
-
|
|
133
|
+
})) as TunnelBackendResult | undefined;
|
|
134
|
+
if (tunnel === undefined) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Tunnel for "${target.name}" could not be started (cloudflared missing or install declined)`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const publicUrl = await resolvePublicUrl(tunnel);
|
|
127
140
|
if (!publicUrl) {
|
|
128
141
|
throw new Error(
|
|
129
142
|
`Tunnel for "${target.name}" did not provide a public URL`,
|
|
130
143
|
);
|
|
131
144
|
}
|
|
132
|
-
|
|
145
|
+
return {
|
|
133
146
|
kind: target.kind,
|
|
134
147
|
name: target.name,
|
|
135
148
|
localUrl,
|
|
136
149
|
publicUrl,
|
|
137
150
|
close: toCloseFn(tunnel),
|
|
138
|
-
}
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const tunnels: PublicTunnel[] = [];
|
|
156
|
+
const errors: unknown[] = [];
|
|
157
|
+
for (const result of settled) {
|
|
158
|
+
if (result.status === "fulfilled") {
|
|
159
|
+
tunnels.push(result.value);
|
|
160
|
+
} else {
|
|
161
|
+
errors.push(result.reason);
|
|
139
162
|
}
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (errors.length > 0) {
|
|
142
166
|
await stopPublicTunnels(tunnels);
|
|
143
|
-
throw
|
|
167
|
+
throw errors[0];
|
|
144
168
|
}
|
|
169
|
+
|
|
170
|
+
return tunnels;
|
|
145
171
|
}
|
|
146
172
|
|
|
147
173
|
export async function stopPublicTunnels(
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
startDevServers,
|
|
13
13
|
stopProcess as stopProcessFn,
|
|
14
14
|
} from "../core/process";
|
|
15
|
+
import {
|
|
16
|
+
type PublicTunnel,
|
|
17
|
+
resolveExposeTargets,
|
|
18
|
+
startPublicTunnels,
|
|
19
|
+
stopPublicTunnels,
|
|
20
|
+
} from "../core/tunnel";
|
|
15
21
|
import { isCI as isCIEnv, logExpoApiUrl, logFrontendPort } from "../core/utils";
|
|
16
22
|
import {
|
|
17
23
|
spawnWatchdog as spawnWatchdogFn,
|
|
@@ -36,15 +42,19 @@ import type {
|
|
|
36
42
|
ComputedUrls,
|
|
37
43
|
DevConfig,
|
|
38
44
|
DevEnvironment,
|
|
45
|
+
DevEnvironmentTunnelLog,
|
|
39
46
|
DevServerPids,
|
|
40
47
|
ExecOptions,
|
|
41
48
|
HookContext,
|
|
49
|
+
OpenPublicTunnelsOptions,
|
|
50
|
+
OpenPublicTunnelsResult,
|
|
42
51
|
PrismaRunner,
|
|
43
52
|
ServiceConfig,
|
|
44
53
|
StartOptions,
|
|
45
54
|
StopOptions,
|
|
46
55
|
} from "../types";
|
|
47
56
|
import { logEnvironmentInfo } from "./logging";
|
|
57
|
+
import { assertOnlyAppNames, pickApps } from "./only-apps";
|
|
48
58
|
import { createCheckTableHelper, createSeedCheckContext } from "./seeding";
|
|
49
59
|
|
|
50
60
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -213,13 +223,18 @@ export function createDevEnvironment<
|
|
|
213
223
|
startServers: shouldStartServers = true,
|
|
214
224
|
productionBuild = isCI,
|
|
215
225
|
skipSeed = false,
|
|
226
|
+
skipEnvironmentLog = false,
|
|
227
|
+
onlyApps,
|
|
216
228
|
} = startOptions;
|
|
217
229
|
|
|
230
|
+
assertOnlyAppNames(Object.keys(apps), onlyApps);
|
|
231
|
+
const appsToStart = pickApps(apps, onlyApps);
|
|
232
|
+
|
|
218
233
|
const envVars = buildEnvVars(productionBuild);
|
|
219
234
|
ensureComposeFile();
|
|
220
235
|
|
|
221
236
|
// Log environment info
|
|
222
|
-
if (verbose) {
|
|
237
|
+
if (verbose && !skipEnvironmentLog) {
|
|
223
238
|
logInfo(productionBuild ? "Production Environment" : "Dev Environment");
|
|
224
239
|
}
|
|
225
240
|
|
|
@@ -330,7 +345,7 @@ export function createDevEnvironment<
|
|
|
330
345
|
}
|
|
331
346
|
|
|
332
347
|
// Start servers if requested
|
|
333
|
-
if (shouldStartServers && Object.keys(
|
|
348
|
+
if (shouldStartServers && Object.keys(appsToStart).length > 0) {
|
|
334
349
|
// Run beforeServers hook
|
|
335
350
|
if (config.hooks?.beforeServers) {
|
|
336
351
|
await config.hooks.beforeServers(getHookContext());
|
|
@@ -338,11 +353,11 @@ export function createDevEnvironment<
|
|
|
338
353
|
|
|
339
354
|
// Build if production
|
|
340
355
|
if (productionBuild) {
|
|
341
|
-
buildApps(
|
|
356
|
+
buildApps(appsToStart, root, envVars, { verbose });
|
|
342
357
|
}
|
|
343
358
|
|
|
344
359
|
// Start servers
|
|
345
|
-
const pids = await startDevServers(
|
|
360
|
+
const pids = await startDevServers(appsToStart, root, envVars, ports, {
|
|
346
361
|
verbose,
|
|
347
362
|
productionBuild,
|
|
348
363
|
isCI,
|
|
@@ -350,7 +365,7 @@ export function createDevEnvironment<
|
|
|
350
365
|
|
|
351
366
|
// Wait for servers to be ready
|
|
352
367
|
if (verbose) console.log("⏳ Waiting for servers to be ready...");
|
|
353
|
-
await waitForDevServers(
|
|
368
|
+
await waitForDevServers(appsToStart, ports, {
|
|
354
369
|
timeout: isCI ? 120000 : 60000,
|
|
355
370
|
verbose,
|
|
356
371
|
productionBuild,
|
|
@@ -400,36 +415,129 @@ export function createDevEnvironment<
|
|
|
400
415
|
// ─────────────────────────────────────────────────────────────────────────
|
|
401
416
|
|
|
402
417
|
async function startServersOnly(
|
|
403
|
-
options: {
|
|
418
|
+
options: {
|
|
419
|
+
productionBuild?: boolean;
|
|
420
|
+
verbose?: boolean;
|
|
421
|
+
onlyApps?: string[];
|
|
422
|
+
} = {},
|
|
404
423
|
): Promise<DevServerPids> {
|
|
405
|
-
const { productionBuild = false, verbose = true } = options;
|
|
424
|
+
const { productionBuild = false, verbose = true, onlyApps } = options;
|
|
425
|
+
assertOnlyAppNames(Object.keys(apps), onlyApps);
|
|
426
|
+
const appsToStart = pickApps(apps, onlyApps);
|
|
406
427
|
const envVars = buildEnvVars(productionBuild);
|
|
407
428
|
const isCI = process.env.CI === "true";
|
|
408
429
|
|
|
430
|
+
if (Object.keys(appsToStart).length === 0) {
|
|
431
|
+
return {};
|
|
432
|
+
}
|
|
433
|
+
|
|
409
434
|
// Build if production
|
|
410
435
|
if (productionBuild) {
|
|
411
|
-
buildApps(
|
|
436
|
+
buildApps(appsToStart, root, envVars, { verbose });
|
|
412
437
|
}
|
|
413
438
|
|
|
414
|
-
|
|
439
|
+
const pids = await startDevServers(appsToStart, root, envVars, ports, {
|
|
415
440
|
verbose,
|
|
416
441
|
productionBuild,
|
|
417
442
|
isCI,
|
|
418
443
|
});
|
|
444
|
+
|
|
445
|
+
if (verbose) console.log("⏳ Waiting for servers to be ready...");
|
|
446
|
+
await waitForDevServers(appsToStart, ports, {
|
|
447
|
+
timeout: isCI ? 120000 : 60000,
|
|
448
|
+
verbose,
|
|
449
|
+
productionBuild,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return pids;
|
|
419
453
|
}
|
|
420
454
|
|
|
421
455
|
async function waitForServersReady(
|
|
422
|
-
options: {
|
|
456
|
+
options: {
|
|
457
|
+
timeout?: number;
|
|
458
|
+
productionBuild?: boolean;
|
|
459
|
+
onlyApps?: string[];
|
|
460
|
+
} = {},
|
|
423
461
|
): Promise<void> {
|
|
424
|
-
const { timeout = 60000, productionBuild = false } = options;
|
|
425
|
-
|
|
462
|
+
const { timeout = 60000, productionBuild = false, onlyApps } = options;
|
|
463
|
+
assertOnlyAppNames(Object.keys(apps), onlyApps);
|
|
464
|
+
const appsToWait = pickApps(apps, onlyApps);
|
|
465
|
+
await waitForDevServers(appsToWait, ports, { timeout, productionBuild });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function openPublicTunnels(
|
|
469
|
+
options: OpenPublicTunnelsOptions = {},
|
|
470
|
+
): Promise<OpenPublicTunnelsResult<TServices, TApps>> {
|
|
471
|
+
const { names, waitForHealthy } = options;
|
|
472
|
+
const exposeList = names?.length ? names.join(",") : undefined;
|
|
473
|
+
|
|
474
|
+
if (waitForHealthy?.length) {
|
|
475
|
+
assertOnlyAppNames(Object.keys(apps), waitForHealthy);
|
|
476
|
+
const appsWait = pickApps(apps, waitForHealthy);
|
|
477
|
+
const isCI = process.env.CI === "true";
|
|
478
|
+
await waitForDevServers(appsWait, ports, {
|
|
479
|
+
timeout: isCI ? 120000 : 60000,
|
|
480
|
+
verbose: config.options?.verbose ?? true,
|
|
481
|
+
productionBuild: false,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const { targets, unknownNames, notEnabledNames } = resolveExposeTargets(
|
|
486
|
+
{
|
|
487
|
+
services,
|
|
488
|
+
apps,
|
|
489
|
+
ports,
|
|
490
|
+
} as DevEnvironment<TServices, TApps>,
|
|
491
|
+
exposeList,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (unknownNames.length > 0) {
|
|
495
|
+
throw new Error(`Unknown expose target(s): ${unknownNames.join(", ")}`);
|
|
496
|
+
}
|
|
497
|
+
if (notEnabledNames.length > 0) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`Target(s) missing expose: true: ${notEnabledNames.join(", ")}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (targets.length === 0) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"No expose targets selected. Add expose: true to services/apps or pass names that have expose: true.",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const tunnels = await startPublicTunnels(targets);
|
|
509
|
+
setPublicUrls(
|
|
510
|
+
Object.fromEntries(tunnels.map((t) => [t.name, t.publicUrl])),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
let closed = false;
|
|
514
|
+
async function close(): Promise<void> {
|
|
515
|
+
if (closed) return;
|
|
516
|
+
closed = true;
|
|
517
|
+
await stopPublicTunnels(tunnels);
|
|
518
|
+
clearPublicUrls();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
publicUrls: { ...publicUrls } as ComputedPublicUrls<TServices, TApps>,
|
|
523
|
+
tunnels,
|
|
524
|
+
close,
|
|
525
|
+
};
|
|
426
526
|
}
|
|
427
527
|
|
|
428
528
|
// ─────────────────────────────────────────────────────────────────────────
|
|
429
529
|
// Utilities
|
|
430
530
|
// ─────────────────────────────────────────────────────────────────────────
|
|
431
531
|
|
|
432
|
-
function logInfo(label = "Docker Dev"): void {
|
|
532
|
+
function logInfo(label = "Docker Dev", tunnels?: PublicTunnel[]): void {
|
|
533
|
+
const tunnelRows: DevEnvironmentTunnelLog[] | undefined = tunnels?.map(
|
|
534
|
+
({ kind, name, localUrl, publicUrl }) => ({
|
|
535
|
+
kind,
|
|
536
|
+
name,
|
|
537
|
+
localUrl,
|
|
538
|
+
publicUrl,
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
433
541
|
logEnvironmentInfo({
|
|
434
542
|
label,
|
|
435
543
|
projectName,
|
|
@@ -440,6 +548,7 @@ export function createDevEnvironment<
|
|
|
440
548
|
worktree,
|
|
441
549
|
portOffset,
|
|
442
550
|
projectSuffix,
|
|
551
|
+
tunnels: tunnelRows,
|
|
443
552
|
});
|
|
444
553
|
}
|
|
445
554
|
|
|
@@ -539,6 +648,7 @@ export function createDevEnvironment<
|
|
|
539
648
|
exec,
|
|
540
649
|
waitForServer: waitForServerUrl,
|
|
541
650
|
logInfo,
|
|
651
|
+
openPublicTunnels,
|
|
542
652
|
|
|
543
653
|
// Vibe Kanban Integration
|
|
544
654
|
getExpoApiUrl,
|