@t3x-dev/local 0.1.2
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/README.md +12 -0
- package/dist/bin/t3x-local.d.ts +1 -0
- package/dist/bin/t3x-local.js +217 -0
- package/dist/bin/t3x-mcp.d.ts +1 -0
- package/dist/bin/t3x-mcp.js +19 -0
- package/dist/bin/t3x.d.ts +1 -0
- package/dist/bin/t3x.js +20 -0
- package/dist/chunk-E4LL4ESG.js +381 -0
- package/dist/chunk-KNAXB6AU.js +573 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/package.json +57 -0
- package/runtime-manifest.json +38 -0
- package/scripts/generate-runtime-manifest.mjs +121 -0
- package/scripts/postinstall-download.mjs +209 -0
- package/scripts/postpack-restore-package-json.mjs +18 -0
- package/scripts/prepack-rewrite-package-json.mjs +31 -0
- package/scripts/prepare-runtime.mjs +78 -0
- package/scripts/runtime-helpers.mjs +115 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearRuntimeState,
|
|
3
|
+
ensureRuntimeMetadataDirs,
|
|
4
|
+
formatMissingArtifacts,
|
|
5
|
+
getLocalPaths,
|
|
6
|
+
getMissingStartArtifacts,
|
|
7
|
+
getRuntimeProcessStatus,
|
|
8
|
+
readRuntimeState,
|
|
9
|
+
spawnNodeScript,
|
|
10
|
+
terminateProcess,
|
|
11
|
+
writeRuntimeState
|
|
12
|
+
} from "./chunk-E4LL4ESG.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/start.ts
|
|
15
|
+
import fs2 from "fs/promises";
|
|
16
|
+
import path3 from "path";
|
|
17
|
+
|
|
18
|
+
// src/runtime/env.ts
|
|
19
|
+
import path from "path";
|
|
20
|
+
var DEFAULT_API_PORT = 8e3;
|
|
21
|
+
var DEFAULT_WEB_PORT = 3e3;
|
|
22
|
+
function resolveStartOptions(input, paths, baseEnv = process.env) {
|
|
23
|
+
const apiPort = input.apiPort ?? DEFAULT_API_PORT;
|
|
24
|
+
const webPort = input.webPort ?? DEFAULT_WEB_PORT;
|
|
25
|
+
validatePort(apiPort, "API");
|
|
26
|
+
validatePort(webPort, "Web");
|
|
27
|
+
if (apiPort === webPort) {
|
|
28
|
+
throw new Error("[t3x-local] API port and Web port must be different");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
dataDir: path.resolve(input.dataDir ?? baseEnv.T3X_DATA_DIR ?? paths.defaultDataDir),
|
|
32
|
+
apiPort,
|
|
33
|
+
webPort
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildApiEnv(baseEnv, options) {
|
|
37
|
+
return {
|
|
38
|
+
...baseEnv,
|
|
39
|
+
NODE_ENV: "development",
|
|
40
|
+
PORT: String(options.apiPort),
|
|
41
|
+
AUTH_DISABLED: "true",
|
|
42
|
+
NEXT_PUBLIC_AUTH_DISABLED: "true",
|
|
43
|
+
T3X_DATA_DIR: options.dataDir
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function buildWebEnv(baseEnv, options) {
|
|
47
|
+
return {
|
|
48
|
+
...baseEnv,
|
|
49
|
+
NODE_ENV: "production",
|
|
50
|
+
PORT: String(options.webPort),
|
|
51
|
+
HOSTNAME: "0.0.0.0",
|
|
52
|
+
AUTH_DISABLED: "true",
|
|
53
|
+
NEXT_PUBLIC_AUTH_DISABLED: "true",
|
|
54
|
+
NEXT_PUBLIC_API_URL: `http://localhost:${options.apiPort}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function validatePort(port, label) {
|
|
58
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
59
|
+
throw new Error(`[t3x-local] ${label} port must be an integer between 1 and 65535`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/runtime/health.ts
|
|
64
|
+
async function waitForHttpOk(url, options) {
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
66
|
+
const intervalMs = options.intervalMs ?? 500;
|
|
67
|
+
const startedAt = Date.now();
|
|
68
|
+
let lastError = null;
|
|
69
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url);
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
lastError = `HTTP ${response.status}`;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
78
|
+
}
|
|
79
|
+
await sleep(intervalMs);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${options.label} did not become healthy at ${url} within ${timeoutMs}ms` + (lastError ? ` (last error: ${lastError})` : "")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
async function checkHttpHealth(url, timeoutMs = 2e3) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
controller.abort();
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
92
|
+
return {
|
|
93
|
+
ok: response.ok,
|
|
94
|
+
details: `HTTP ${response.status}`
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
details: error instanceof Error ? error.message : String(error)
|
|
100
|
+
};
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function sleep(ms) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
setTimeout(resolve, ms);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/runtime/ports.ts
|
|
112
|
+
import net from "net";
|
|
113
|
+
async function getPortStatus(port) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const socket = net.connect({
|
|
116
|
+
host: "127.0.0.1",
|
|
117
|
+
port
|
|
118
|
+
});
|
|
119
|
+
socket.setTimeout(1500);
|
|
120
|
+
socket.once("connect", () => {
|
|
121
|
+
socket.end();
|
|
122
|
+
resolve({
|
|
123
|
+
port,
|
|
124
|
+
available: false,
|
|
125
|
+
details: "occupied"
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
socket.once("timeout", () => {
|
|
129
|
+
socket.destroy();
|
|
130
|
+
resolve({
|
|
131
|
+
port,
|
|
132
|
+
available: false,
|
|
133
|
+
details: "timeout"
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
socket.once("error", (error) => {
|
|
137
|
+
if (error.code === "ECONNREFUSED") {
|
|
138
|
+
resolve({
|
|
139
|
+
port,
|
|
140
|
+
available: true,
|
|
141
|
+
details: "available"
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
resolve({
|
|
146
|
+
port,
|
|
147
|
+
available: false,
|
|
148
|
+
details: error.code ?? error.message
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function assertPortAvailable(port, label) {
|
|
154
|
+
const status = await getPortStatus(port);
|
|
155
|
+
if (!status.available) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[t3x-local] ${label} port ${port} is already in use (${status.details}). Choose a different port or stop the conflicting process first.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/runtime/version-check.ts
|
|
163
|
+
import fs from "fs";
|
|
164
|
+
import { createRequire } from "module";
|
|
165
|
+
import path2 from "path";
|
|
166
|
+
var require2 = createRequire(import.meta.url);
|
|
167
|
+
var FIXED_VERSION_PACKAGES = [
|
|
168
|
+
"@t3x-dev/yops",
|
|
169
|
+
"@t3x-dev/yschema",
|
|
170
|
+
"@t3x-dev/core",
|
|
171
|
+
"@t3x-dev/storage",
|
|
172
|
+
"@t3x-dev/api",
|
|
173
|
+
"@t3x-dev/api-client",
|
|
174
|
+
"@t3x-dev/cli",
|
|
175
|
+
"@t3x-dev/mcp",
|
|
176
|
+
"@t3x-dev/local"
|
|
177
|
+
];
|
|
178
|
+
var FIXED_PACKAGE_WORKSPACE_PATHS = {
|
|
179
|
+
"@t3x-dev/yops": path2.join("packages", "yops", "package.json"),
|
|
180
|
+
"@t3x-dev/yschema": path2.join("packages", "yschema", "package.json"),
|
|
181
|
+
"@t3x-dev/core": path2.join("packages", "core", "package.json"),
|
|
182
|
+
"@t3x-dev/storage": path2.join("packages", "storage", "package.json"),
|
|
183
|
+
"@t3x-dev/api": path2.join("packages", "api", "package.json"),
|
|
184
|
+
"@t3x-dev/api-client": path2.join("packages", "api-client", "package.json"),
|
|
185
|
+
"@t3x-dev/cli": path2.join("apps", "cli", "package.json"),
|
|
186
|
+
"@t3x-dev/mcp": path2.join("apps", "mcp", "package.json"),
|
|
187
|
+
"@t3x-dev/local": path2.join("apps", "local", "package.json")
|
|
188
|
+
};
|
|
189
|
+
var LOCAL_DIRECT_FIXED_DEPENDENCIES = [
|
|
190
|
+
"@t3x-dev/api",
|
|
191
|
+
"@t3x-dev/cli",
|
|
192
|
+
"@t3x-dev/mcp",
|
|
193
|
+
"@t3x-dev/storage"
|
|
194
|
+
];
|
|
195
|
+
function getVersionSnapshot(paths) {
|
|
196
|
+
const report = getVersionLockReport(paths);
|
|
197
|
+
const manifestVersions = readManifestDependencyVersions(paths);
|
|
198
|
+
return {
|
|
199
|
+
node: process.version,
|
|
200
|
+
platform: `${process.platform}-${process.arch}`,
|
|
201
|
+
local: report.resolvedVersions["@t3x-dev/local"] ?? "unknown",
|
|
202
|
+
api: report.resolvedVersions["@t3x-dev/api"] ?? "unknown",
|
|
203
|
+
web: manifestVersions.web ?? (paths.repoRoot ? readPackageVersion(path2.join(paths.repoRoot, "apps", "web", "package.json")) : "runtime-only"),
|
|
204
|
+
cli: report.resolvedVersions["@t3x-dev/cli"] ?? "unknown",
|
|
205
|
+
mcp: report.resolvedVersions["@t3x-dev/mcp"] ?? "unknown",
|
|
206
|
+
fixedVersion: report.expectedVersion
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function getVersionLockReport(paths) {
|
|
210
|
+
const localPackageJsonPath = path2.join(paths.packageDir, "package.json");
|
|
211
|
+
const localPackageJson = readPackageJson(localPackageJsonPath);
|
|
212
|
+
const manifest = readRuntimeManifest(paths);
|
|
213
|
+
const expectedVersion = localPackageJson.version ?? "unknown";
|
|
214
|
+
const problems = [];
|
|
215
|
+
const resolvedVersions = {};
|
|
216
|
+
for (const dependencyName of LOCAL_DIRECT_FIXED_DEPENDENCIES) {
|
|
217
|
+
const actual = localPackageJson.dependencies?.[dependencyName];
|
|
218
|
+
if (actual !== expectedVersion && actual !== `workspace:${expectedVersion}`) {
|
|
219
|
+
problems.push(
|
|
220
|
+
`Local package dependency ${dependencyName} must pin ${expectedVersion}, found ${actual ?? "missing"}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (manifest) {
|
|
225
|
+
if (manifest.packageVersion !== expectedVersion) {
|
|
226
|
+
problems.push(
|
|
227
|
+
`runtime-manifest.json packageVersion must be ${expectedVersion}, found ${manifest.packageVersion ?? "missing"}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
for (const packageName of FIXED_VERSION_PACKAGES) {
|
|
231
|
+
const manifestVersion = manifest.dependencies?.[packageName];
|
|
232
|
+
if (manifestVersion !== expectedVersion) {
|
|
233
|
+
problems.push(
|
|
234
|
+
`runtime-manifest.json dependency ${packageName} must be ${expectedVersion}, found ${manifestVersion ?? "missing"}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const packageName of FIXED_VERSION_PACKAGES) {
|
|
240
|
+
const actualVersion = packageName === "@t3x-dev/local" ? expectedVersion : resolveFixedPackageVersion(packageName, paths, manifest);
|
|
241
|
+
resolvedVersions[packageName] = actualVersion ?? "unknown";
|
|
242
|
+
if (!actualVersion) {
|
|
243
|
+
problems.push(`Could not resolve installed version for ${packageName}`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (actualVersion !== expectedVersion) {
|
|
247
|
+
problems.push(
|
|
248
|
+
`${packageName} must use fixed version ${expectedVersion}, found ${actualVersion}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
expectedVersion,
|
|
254
|
+
resolvedVersions,
|
|
255
|
+
problems
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function assertVersionLockOrThrow(paths, context) {
|
|
259
|
+
const report = getVersionLockReport(paths);
|
|
260
|
+
if (report.problems.length > 0) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`[t3x-local] Version lock check failed during ${context}.
|
|
263
|
+
` + report.problems.map((problem) => `- ${problem}`).join("\n")
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return report;
|
|
267
|
+
}
|
|
268
|
+
function resolveFixedPackageVersion(packageName, paths, manifest) {
|
|
269
|
+
const installedPackageJsonPath = findInstalledPackageJson(packageName);
|
|
270
|
+
if (installedPackageJsonPath) {
|
|
271
|
+
return readPackageVersion(installedPackageJsonPath);
|
|
272
|
+
}
|
|
273
|
+
if (paths.repoRoot) {
|
|
274
|
+
return readPackageVersion(
|
|
275
|
+
path2.join(paths.repoRoot, FIXED_PACKAGE_WORKSPACE_PATHS[packageName])
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return manifest?.dependencies?.[packageName] ?? null;
|
|
279
|
+
}
|
|
280
|
+
function readRuntimeManifest(paths) {
|
|
281
|
+
if (!fs.existsSync(paths.runtimeManifestPath)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return JSON.parse(fs.readFileSync(paths.runtimeManifestPath, "utf8"));
|
|
285
|
+
}
|
|
286
|
+
function readManifestDependencyVersions(paths) {
|
|
287
|
+
return readRuntimeManifest(paths)?.dependencies ?? {};
|
|
288
|
+
}
|
|
289
|
+
function findInstalledPackageJson(packageName) {
|
|
290
|
+
try {
|
|
291
|
+
const entryPath = require2.resolve(packageName);
|
|
292
|
+
let current = path2.dirname(entryPath);
|
|
293
|
+
while (current !== path2.dirname(current)) {
|
|
294
|
+
const packageJsonPath = path2.join(current, "package.json");
|
|
295
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
296
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
297
|
+
if (packageJson.name === packageName) {
|
|
298
|
+
return packageJsonPath;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
current = path2.dirname(current);
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function readPackageJson(packageJsonPath) {
|
|
309
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
310
|
+
}
|
|
311
|
+
function readPackageVersion(packageJsonPath) {
|
|
312
|
+
return readPackageJson(packageJsonPath).version ?? "unknown";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/commands/start.ts
|
|
316
|
+
var TEXT_RUNTIME_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".js", ".json", ".mjs"]);
|
|
317
|
+
var BAKED_API_URL_PATTERNS = [
|
|
318
|
+
{
|
|
319
|
+
from: "http://localhost:8000",
|
|
320
|
+
to: (apiPort) => `http://localhost:${apiPort}`
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
from: "http://127.0.0.1:8000",
|
|
324
|
+
to: (apiPort) => `http://127.0.0.1:${apiPort}`
|
|
325
|
+
}
|
|
326
|
+
];
|
|
327
|
+
async function runStartCommand(input = {}) {
|
|
328
|
+
const paths = getLocalPaths();
|
|
329
|
+
assertVersionLockOrThrow(paths, "t3x-local start");
|
|
330
|
+
const options = resolveStartOptions(input, paths, process.env);
|
|
331
|
+
const missing = getMissingStartArtifacts(paths);
|
|
332
|
+
if (missing.length > 0) {
|
|
333
|
+
throw new Error(formatMissingArtifacts(missing, paths));
|
|
334
|
+
}
|
|
335
|
+
const existingState = await readRuntimeState(paths);
|
|
336
|
+
if (existingState) {
|
|
337
|
+
const status = getRuntimeProcessStatus(existingState);
|
|
338
|
+
if (status.apiRunning || status.webRunning) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`[t3x-local] Local runtime is already running. API pid=${existingState.apiPid}, Web pid=${existingState.webPid}. Run \`t3x-local doctor\` or \`t3x-local stop\` first.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
await clearRuntimeState(paths);
|
|
344
|
+
}
|
|
345
|
+
await Promise.all([
|
|
346
|
+
assertPortAvailable(options.apiPort, "API"),
|
|
347
|
+
assertPortAvailable(options.webPort, "Web")
|
|
348
|
+
]);
|
|
349
|
+
const metadataPaths = await ensureRuntimeMetadataDirs(paths);
|
|
350
|
+
let apiProcess = null;
|
|
351
|
+
let webProcess = null;
|
|
352
|
+
try {
|
|
353
|
+
await prepareWebRuntime(paths, options);
|
|
354
|
+
apiProcess = spawnApi(paths, options, metadataPaths.apiLogPath);
|
|
355
|
+
webProcess = spawnWeb(paths, options, metadataPaths.webLogPath);
|
|
356
|
+
if (!apiProcess.child.pid || !webProcess.child.pid) {
|
|
357
|
+
throw new Error("[t3x-local] Failed to capture child process IDs for the local runtime");
|
|
358
|
+
}
|
|
359
|
+
const runtimeState = buildRuntimeState(
|
|
360
|
+
options,
|
|
361
|
+
metadataPaths.apiLogPath,
|
|
362
|
+
metadataPaths.webLogPath,
|
|
363
|
+
{
|
|
364
|
+
apiPid: apiProcess.child.pid,
|
|
365
|
+
webPid: webProcess.child.pid
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
await clearRuntimeState(paths);
|
|
369
|
+
await Promise.all([
|
|
370
|
+
writeRuntimeState(paths, runtimeState),
|
|
371
|
+
waitForHttpOk(runtimeState.apiHealthUrl, { label: "API" }),
|
|
372
|
+
waitForHttpOk(runtimeState.webHealthUrl, { label: "Web" })
|
|
373
|
+
]);
|
|
374
|
+
console.log(`[t3x-local] Started API pid ${runtimeState.apiPid} at ${runtimeState.apiUrl}`);
|
|
375
|
+
console.log(`[t3x-local] Started Web pid ${runtimeState.webPid} at ${runtimeState.webUrl}`);
|
|
376
|
+
console.log(`[t3x-local] Data dir: ${runtimeState.dataDir}`);
|
|
377
|
+
console.log(`[t3x-local] State file: ${metadataPaths.stateFilePath}`);
|
|
378
|
+
console.log(`[t3x-local] Logs: ${metadataPaths.apiLogPath} | ${metadataPaths.webLogPath}`);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
await Promise.all([
|
|
381
|
+
apiProcess ? terminateProcess(apiProcess) : Promise.resolve(),
|
|
382
|
+
webProcess ? terminateProcess(webProcess) : Promise.resolve(),
|
|
383
|
+
clearRuntimeState(paths)
|
|
384
|
+
]);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function buildRuntimeState(options, apiLogPath, webLogPath, pids) {
|
|
389
|
+
return {
|
|
390
|
+
schemaVersion: 1,
|
|
391
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
392
|
+
dataDir: options.dataDir,
|
|
393
|
+
apiPort: options.apiPort,
|
|
394
|
+
webPort: options.webPort,
|
|
395
|
+
apiPid: pids.apiPid,
|
|
396
|
+
webPid: pids.webPid,
|
|
397
|
+
apiUrl: `http://localhost:${options.apiPort}`,
|
|
398
|
+
webUrl: `http://localhost:${options.webPort}`,
|
|
399
|
+
apiHealthUrl: `http://127.0.0.1:${options.apiPort}/health`,
|
|
400
|
+
webHealthUrl: `http://127.0.0.1:${options.webPort}/health`,
|
|
401
|
+
apiLogPath,
|
|
402
|
+
webLogPath
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function spawnApi(paths, options, logPath) {
|
|
406
|
+
console.log("[t3x-local] Starting API...");
|
|
407
|
+
return spawnNodeScript({
|
|
408
|
+
name: "api",
|
|
409
|
+
entryPath: paths.apiEntryPath,
|
|
410
|
+
cwd: paths.repoRoot ?? paths.packageDir,
|
|
411
|
+
detached: true,
|
|
412
|
+
env: buildApiEnv(process.env, options),
|
|
413
|
+
stderrPath: logPath,
|
|
414
|
+
stdoutPath: logPath
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function spawnWeb(paths, options, logPath) {
|
|
418
|
+
console.log("[t3x-local] Starting Web...");
|
|
419
|
+
const runtimeDir = `${paths.localRuntimeRoot}/web`;
|
|
420
|
+
const webEntryPath = `${runtimeDir}/apps/web/server.js`;
|
|
421
|
+
return spawnNodeScript({
|
|
422
|
+
name: "web",
|
|
423
|
+
entryPath: webEntryPath,
|
|
424
|
+
cwd: runtimeDir,
|
|
425
|
+
detached: true,
|
|
426
|
+
env: buildWebEnv(process.env, options),
|
|
427
|
+
stderrPath: logPath,
|
|
428
|
+
stdoutPath: logPath
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async function prepareWebRuntime(paths, options) {
|
|
432
|
+
const runtimeDir = `${paths.localRuntimeRoot}/web`;
|
|
433
|
+
const runtimeNextDir = `${runtimeDir}/apps/web/.next`;
|
|
434
|
+
await fs2.rm(runtimeDir, { recursive: true, force: true });
|
|
435
|
+
await fs2.cp(paths.webStandaloneDir, runtimeDir, {
|
|
436
|
+
recursive: true,
|
|
437
|
+
verbatimSymlinks: true
|
|
438
|
+
});
|
|
439
|
+
await fs2.mkdir(runtimeNextDir, { recursive: true });
|
|
440
|
+
await fs2.cp(paths.webStaticDir, `${runtimeNextDir}/static`, { recursive: true });
|
|
441
|
+
await fs2.cp(paths.webPublicDir, `${runtimeDir}/apps/web/public`, { recursive: true });
|
|
442
|
+
const rewriteStats = await rewriteBakedWebApiUrls(runtimeDir, options.apiPort);
|
|
443
|
+
validateRewriteOutcome(rewriteStats, options.apiPort);
|
|
444
|
+
await assertNoBakedApiUrlResidue(runtimeDir, options.apiPort);
|
|
445
|
+
if (rewriteStats.replacements > 0) {
|
|
446
|
+
console.log(
|
|
447
|
+
`[t3x-local] Rewrote ${rewriteStats.replacements} baked Web API URL occurrence(s) across ${rewriteStats.filesUpdated} file(s).`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function rewriteBakedWebApiUrls(runtimeDir, apiPort) {
|
|
452
|
+
const stats = {
|
|
453
|
+
filesScanned: 0,
|
|
454
|
+
filesUpdated: 0,
|
|
455
|
+
replacements: 0
|
|
456
|
+
};
|
|
457
|
+
await rewriteRuntimeTree(runtimeDir, apiPort, stats);
|
|
458
|
+
return stats;
|
|
459
|
+
}
|
|
460
|
+
async function rewriteRuntimeTree(currentDir, apiPort, stats) {
|
|
461
|
+
const entries = await fs2.readdir(currentDir, { withFileTypes: true });
|
|
462
|
+
await Promise.all(
|
|
463
|
+
entries.map(async (entry) => {
|
|
464
|
+
const entryPath = path3.join(currentDir, entry.name);
|
|
465
|
+
if (entry.isDirectory()) {
|
|
466
|
+
await rewriteRuntimeTree(entryPath, apiPort, stats);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (!shouldRewriteRuntimeFile(entryPath)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
stats.filesScanned += 1;
|
|
473
|
+
const original = await fs2.readFile(entryPath, "utf8");
|
|
474
|
+
const rewritten = replaceBakedApiUrls(original, apiPort);
|
|
475
|
+
if (rewritten === original) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
stats.filesUpdated += 1;
|
|
479
|
+
stats.replacements += countApiUrlReplacements(original, apiPort);
|
|
480
|
+
await fs2.writeFile(entryPath, rewritten);
|
|
481
|
+
})
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
function shouldRewriteRuntimeFile(filePath) {
|
|
485
|
+
return TEXT_RUNTIME_EXTENSIONS.has(path3.extname(filePath));
|
|
486
|
+
}
|
|
487
|
+
function replaceBakedApiUrls(source, apiPort) {
|
|
488
|
+
let rewritten = source;
|
|
489
|
+
for (const pattern of BAKED_API_URL_PATTERNS) {
|
|
490
|
+
rewritten = rewritten.replaceAll(pattern.from, pattern.to(apiPort));
|
|
491
|
+
}
|
|
492
|
+
return rewritten;
|
|
493
|
+
}
|
|
494
|
+
function countApiUrlReplacements(source, apiPort) {
|
|
495
|
+
let replacements = 0;
|
|
496
|
+
for (const pattern of BAKED_API_URL_PATTERNS) {
|
|
497
|
+
if (pattern.from === pattern.to(apiPort)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
replacements += countOccurrences(source, pattern.from);
|
|
501
|
+
}
|
|
502
|
+
return replacements;
|
|
503
|
+
}
|
|
504
|
+
function countOccurrences(source, needle) {
|
|
505
|
+
if (!needle) {
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
let count = 0;
|
|
509
|
+
let cursor = 0;
|
|
510
|
+
while (cursor < source.length) {
|
|
511
|
+
const next = source.indexOf(needle, cursor);
|
|
512
|
+
if (next === -1) {
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
count += 1;
|
|
516
|
+
cursor = next + needle.length;
|
|
517
|
+
}
|
|
518
|
+
return count;
|
|
519
|
+
}
|
|
520
|
+
function validateRewriteOutcome(stats, apiPort) {
|
|
521
|
+
if (apiPort === DEFAULT_API_PORT) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (stats.replacements === 0) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`[t3x-local] The copied Web runtime does not contain the expected baked API URL. Refuse to start with a non-default \`--api-port\` because the browser bundle may still call http://localhost:${DEFAULT_API_PORT}. Rebuild apps/web or use the default API port.`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function assertNoBakedApiUrlResidue(runtimeDir, apiPort) {
|
|
531
|
+
if (apiPort === DEFAULT_API_PORT) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const residue = await findBakedApiUrlResidue(runtimeDir);
|
|
535
|
+
if (residue.length === 0) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const details = residue.map(({ filePath, url }) => `- ${filePath}: ${url}`).join("\n");
|
|
539
|
+
throw new Error(
|
|
540
|
+
`[t3x-local] Refuse to start because the copied Web runtime still contains baked API URLs for port ${DEFAULT_API_PORT}.
|
|
541
|
+
${details}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
async function findBakedApiUrlResidue(currentDir) {
|
|
545
|
+
const entries = await fs2.readdir(currentDir, { withFileTypes: true });
|
|
546
|
+
const residue = [];
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
const entryPath = path3.join(currentDir, entry.name);
|
|
549
|
+
if (entry.isDirectory()) {
|
|
550
|
+
residue.push(...await findBakedApiUrlResidue(entryPath));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!shouldRewriteRuntimeFile(entryPath)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const contents = await fs2.readFile(entryPath, "utf8");
|
|
557
|
+
for (const pattern of BAKED_API_URL_PATTERNS) {
|
|
558
|
+
if (contents.includes(pattern.from)) {
|
|
559
|
+
residue.push({ filePath: entryPath, url: pattern.from });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return residue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export {
|
|
567
|
+
resolveStartOptions,
|
|
568
|
+
checkHttpHealth,
|
|
569
|
+
getPortStatus,
|
|
570
|
+
getVersionSnapshot,
|
|
571
|
+
getVersionLockReport,
|
|
572
|
+
runStartCommand
|
|
573
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@t3x-dev/local",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "T3X local entry package",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"t3x-local": "./dist/bin/t3x-local.js",
|
|
9
|
+
"t3x": "./dist/bin/t3x.js",
|
|
10
|
+
"t3x-mcp": "./dist/bin/t3x-mcp.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"scripts",
|
|
17
|
+
"runtime-manifest.json"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/t3x-dev/t3x-core.git",
|
|
25
|
+
"directory": "apps/local"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts src/bin/t3x-local.ts src/bin/t3x.ts src/bin/t3x-mcp.ts --format esm --dts --clean",
|
|
29
|
+
"dev": "tsx src/bin/t3x-local.ts",
|
|
30
|
+
"prepare:runtime": "node ./scripts/prepare-runtime.mjs",
|
|
31
|
+
"generate:runtime-manifest": "node ./scripts/generate-runtime-manifest.mjs",
|
|
32
|
+
"prepack": "node ../../tools/rewrite-workspace-package-json.mjs",
|
|
33
|
+
"postpack": "node ../../tools/restore-package-json.mjs",
|
|
34
|
+
"start": "node dist/bin/t3x-local.js start",
|
|
35
|
+
"test": "vitest run --passWithNoTests",
|
|
36
|
+
"lint": "biome lint src",
|
|
37
|
+
"postinstall": "node ./scripts/postinstall-download.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@hono/node-server": "^1.15.2",
|
|
41
|
+
"@t3x-dev/api": "0.1.2",
|
|
42
|
+
"@t3x-dev/cli": "0.1.2",
|
|
43
|
+
"@t3x-dev/mcp": "0.1.2",
|
|
44
|
+
"@t3x-dev/storage": "0.1.2",
|
|
45
|
+
"commander": "^13.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"tsx": "^4.19.2",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^2.1.9"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.9.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifestVersion": 1,
|
|
3
|
+
"packageVersion": "0.1.2",
|
|
4
|
+
"fixedVersion": "0.1.2",
|
|
5
|
+
"generatedAt": "2026-04-23T09:12:14.667Z",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@t3x-dev/yops": "0.1.2",
|
|
8
|
+
"@t3x-dev/yschema": "0.1.2",
|
|
9
|
+
"@t3x-dev/core": "0.1.2",
|
|
10
|
+
"@t3x-dev/storage": "0.1.2",
|
|
11
|
+
"@t3x-dev/api": "0.1.2",
|
|
12
|
+
"@t3x-dev/api-client": "0.1.2",
|
|
13
|
+
"@t3x-dev/cli": "0.1.2",
|
|
14
|
+
"@t3x-dev/mcp": "0.1.2",
|
|
15
|
+
"@t3x-dev/local": "0.1.2",
|
|
16
|
+
"web": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"platforms": {
|
|
19
|
+
"darwin-arm64": {
|
|
20
|
+
"fileName": "t3x-local-runtime-0.1.2-darwin-arm64.tar.gz",
|
|
21
|
+
"url": "https://github.com/t3x-dev/t3x-core/releases/download/t3x-local-v0.1.2/t3x-local-runtime-0.1.2-darwin-arm64.tar.gz",
|
|
22
|
+
"sha256": "d90390ca1a90be083a78d4caf670d027985084eafbbfbc001fb84f30565b7a85",
|
|
23
|
+
"size": 20245492
|
|
24
|
+
},
|
|
25
|
+
"linux-arm64": {
|
|
26
|
+
"fileName": "t3x-local-runtime-0.1.2-linux-arm64.tar.gz",
|
|
27
|
+
"url": "https://github.com/t3x-dev/t3x-core/releases/download/t3x-local-v0.1.2/t3x-local-runtime-0.1.2-linux-arm64.tar.gz",
|
|
28
|
+
"sha256": "384afcf94e9208122a995980fbdf9fee072e14514ce269898726fd357e359b2c",
|
|
29
|
+
"size": 28136616
|
|
30
|
+
},
|
|
31
|
+
"linux-x64": {
|
|
32
|
+
"fileName": "t3x-local-runtime-0.1.2-linux-x64.tar.gz",
|
|
33
|
+
"url": "https://github.com/t3x-dev/t3x-core/releases/download/t3x-local-v0.1.2/t3x-local-runtime-0.1.2-linux-x64.tar.gz",
|
|
34
|
+
"sha256": "5ed8c8fb5597c2ecac0593ffc48f604b7484df538965c07d81663d3ad29a7c87",
|
|
35
|
+
"size": 27977895
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|