@fedify/init 2.2.0-dev.635 → 2.2.0-dev.652
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/action/configs.js +1 -1
- package/dist/action/deps.js +3 -2
- package/dist/action/mod.js +1 -3
- package/dist/action/notice.js +1 -1
- package/dist/action/utils.js +22 -1
- package/dist/ask/dir.js +3 -2
- package/dist/command.d.ts +3 -3
- package/dist/const.js +2 -1
- package/dist/deno.js +1 -1
- package/dist/json/deps.js +31 -0
- package/dist/json/deps.json +31 -0
- package/dist/json/kv.js +3 -3
- package/dist/json/kv.json +63 -17
- package/dist/json/mq.js +5 -5
- package/dist/json/mq.json +5 -5
- package/dist/lib.js +5 -8
- package/dist/templates/solidstart/app.config.ts.tpl +8 -0
- package/dist/templates/solidstart/src/app.tsx.tpl +11 -0
- package/dist/templates/solidstart/src/entry-client.tsx.tpl +4 -0
- package/dist/templates/solidstart/src/entry-server.tsx.tpl +20 -0
- package/dist/templates/solidstart/src/middleware/index.ts.tpl +4 -0
- package/dist/templates/solidstart/src/routes/index.tsx.tpl +10 -0
- package/dist/test/create.js +11 -25
- package/dist/test/lookup.js +60 -134
- package/dist/test/mod.js +2 -1
- package/dist/test/port.js +128 -0
- package/dist/test/run.js +1 -1
- package/dist/test/server.js +137 -0
- package/dist/utils.d.ts +1 -3
- package/dist/utils.js +2 -78
- package/dist/webframeworks/astro.js +6 -4
- package/dist/webframeworks/bare-bones.js +9 -8
- package/dist/webframeworks/const.js +3 -1
- package/dist/webframeworks/elysia.js +11 -10
- package/dist/webframeworks/express.js +6 -5
- package/dist/webframeworks/hono.js +12 -11
- package/dist/webframeworks/mod.js +3 -1
- package/dist/webframeworks/next.js +2 -1
- package/dist/webframeworks/solidstart.js +70 -0
- package/package.json +2 -1
- package/dist/action/install.js +0 -18
- package/dist/action/precommand.js +0 -22
package/dist/test/lookup.js
CHANGED
|
@@ -1,185 +1,111 @@
|
|
|
1
|
-
import { printErrorMessage, printMessage
|
|
2
|
-
import { getDevCommand } from "../lib.js";
|
|
1
|
+
import { printErrorMessage, printMessage } from "../utils.js";
|
|
2
|
+
import { getDevCommand, kvStores, messageQueues, packageManagers } from "../lib.js";
|
|
3
3
|
import webFrameworks from "../webframeworks/mod.js";
|
|
4
|
-
import
|
|
4
|
+
import { replacePortInApp, reservePort } from "./port.js";
|
|
5
|
+
import { STARTUP_TIMEOUT, serverClosure, waitForServer } from "./server.js";
|
|
6
|
+
import { join } from "@fxts/core";
|
|
7
|
+
import $ from "@david/dax";
|
|
8
|
+
import { join as join$1, sep } from "node:path";
|
|
5
9
|
import { values } from "@optique/core";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import { createWriteStream } from "node:fs";
|
|
8
|
-
import { join, sep } from "node:path";
|
|
9
|
-
import { isEmpty } from "@fxts/core/index.js";
|
|
10
10
|
//#region src/test/lookup.ts
|
|
11
11
|
const HANDLE = "john";
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const BANNED_LOOKUP_REASONS = {
|
|
13
|
+
"next,*,*,*": "Next.js doesn't support remote packages",
|
|
14
|
+
"solidstart,deno,*,*": "Error occurred while loading submodules in Deno",
|
|
15
|
+
"astro,deno,*,*": "Astro doesn't support remote packages in Deno"
|
|
16
|
+
};
|
|
17
|
+
const BANNED_LOOKUP_CASES = Object.keys(BANNED_LOOKUP_REASONS).map((key) => key.split(","));
|
|
15
18
|
/**
|
|
16
19
|
* Run servers for all generated apps and test them with the lookup command.
|
|
17
20
|
*
|
|
18
21
|
* @param dirs - Array of paths to generated app directories
|
|
19
22
|
*/
|
|
20
23
|
async function runServerAndLookupUser(dirs) {
|
|
21
|
-
const valid = dirs.filter(Boolean);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const
|
|
27
|
-
printMessage`\nLookup Test start for ${String(filtered.length)} app(s)!`;
|
|
28
|
-
const results = await Array.fromAsync(filtered, testApp);
|
|
24
|
+
const valid = dirs.filter(Boolean).filter(isTestable);
|
|
25
|
+
printSkippedCases(dirs);
|
|
26
|
+
if (valid.length === 0) printErrorMessage`\nNo directories to lookup test.`;
|
|
27
|
+
printMessage``;
|
|
28
|
+
printMessage`Lookup Test start for ${String(valid.length)} app(s)!`;
|
|
29
|
+
const results = await Array.fromAsync(valid, testApp);
|
|
29
30
|
const successCount = results.filter(Boolean).length;
|
|
30
31
|
const failCount = results.length - successCount;
|
|
31
32
|
printMessage`Lookup Test Results:
|
|
32
33
|
Total: ${String(results.length)}
|
|
33
34
|
Passed: ${String(successCount)}
|
|
34
35
|
Failed: ${String(failCount)}\n\n`;
|
|
36
|
+
printFailedCases(valid, results);
|
|
37
|
+
}
|
|
38
|
+
const parseLookupCase = (dir) => dir.split(sep).slice(-4);
|
|
39
|
+
const matchesLookupCasePattern = (target) => (pattern) => pattern.every((value, index) => value === "*" || value === target[index]);
|
|
40
|
+
const isTestable = (dir) => !BANNED_LOOKUP_CASES.some(matchesLookupCasePattern(parseLookupCase(dir)));
|
|
41
|
+
function printSkippedCases(dirs) {
|
|
42
|
+
const matchedPatterns = new Set(dirs.filter(Boolean).flatMap((dir) => BANNED_LOOKUP_CASES.filter(matchesLookupCasePattern(parseLookupCase(dir))).map(join(","))));
|
|
43
|
+
if (matchedPatterns.size > 0) {
|
|
44
|
+
printMessage``;
|
|
45
|
+
printMessage`Skipped the following lookup cases due to known issues:`;
|
|
46
|
+
}
|
|
47
|
+
for (const key of matchedPatterns) {
|
|
48
|
+
const reason = BANNED_LOOKUP_REASONS[key] ?? "unknown reason";
|
|
49
|
+
printMessage` - ${values(Array.from(getLabels(key.split(","))))}: ${reason}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function* getLabels([wf, pm, kv, mq]) {
|
|
53
|
+
if (wf !== "*") yield webFrameworks[wf].label;
|
|
54
|
+
if (pm !== "*") yield packageManagers[pm].label;
|
|
55
|
+
if (kv !== "*") yield kvStores[kv].label;
|
|
56
|
+
if (mq !== "*") yield messageQueues[mq].label;
|
|
35
57
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
function printFailedCases(valid, results) {
|
|
59
|
+
if (results.every(Boolean)) return;
|
|
60
|
+
printMessage`Failed cases:`;
|
|
61
|
+
for (let i = 0; i < results.length; i++) if (!results[i]) {
|
|
62
|
+
const dir = valid[i];
|
|
63
|
+
printMessage` - ${values(parseLookupCase(dir))}: ${dir}`;
|
|
64
|
+
}
|
|
42
65
|
}
|
|
43
66
|
/**
|
|
44
67
|
* Run the dev server and test with lookup command.
|
|
45
68
|
*/
|
|
46
69
|
async function testApp(dir) {
|
|
47
|
-
const [wf, pm, kv, mq] = dir
|
|
70
|
+
const [wf, pm, kv, mq] = parseLookupCase(dir);
|
|
48
71
|
printMessage` Testing ${values([
|
|
49
72
|
wf,
|
|
50
73
|
pm,
|
|
51
74
|
kv,
|
|
52
75
|
mq
|
|
53
76
|
])}...`;
|
|
54
|
-
const
|
|
77
|
+
const defaultPort = webFrameworks[wf].defaultPort;
|
|
78
|
+
const { port, release } = await reservePort();
|
|
79
|
+
await replacePortInApp(dir, wf, defaultPort, port);
|
|
80
|
+
printMessage` Using port ${String(port)}`;
|
|
81
|
+
const result = await serverClosure(dir, getDevCommand(pm), port, sendLookup, release).catch(() => false);
|
|
55
82
|
printMessage` Lookup ${result ? "successful" : "failed"} for ${values([
|
|
56
83
|
wf,
|
|
57
84
|
pm,
|
|
58
85
|
kv,
|
|
59
86
|
mq
|
|
60
87
|
])}!`;
|
|
61
|
-
if (!result) printMessage` Check out these files for more details:
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
if (!result) printMessage` Check out these files for more details: \
|
|
89
|
+
${join$1(dir, "out.txt")} and \
|
|
90
|
+
${join$1(dir, "err.txt")}\n`;
|
|
64
91
|
printMessage`\n`;
|
|
65
92
|
return result;
|
|
66
93
|
}
|
|
67
|
-
|
|
94
|
+
async function sendLookup(port) {
|
|
68
95
|
const serverUrl = `http://localhost:${port}`;
|
|
69
96
|
const lookupTarget = `${serverUrl}/users/${HANDLE}`;
|
|
70
97
|
printMessage` Waiting for server to start at ${serverUrl}...`;
|
|
71
|
-
if (!await waitForServer(serverUrl
|
|
72
|
-
printErrorMessage`Server did not start within
|
|
73
|
-
${String(STARTUP_TIMEOUT)}ms`;
|
|
98
|
+
if (!await waitForServer(serverUrl)) {
|
|
99
|
+
printErrorMessage`Server did not start within ${String(STARTUP_TIMEOUT)}ms`;
|
|
74
100
|
return false;
|
|
75
101
|
}
|
|
76
102
|
printMessage` Server is ready. Running lookup command...`;
|
|
77
103
|
try {
|
|
78
|
-
await
|
|
79
|
-
"deno",
|
|
80
|
-
"task",
|
|
81
|
-
"cli",
|
|
82
|
-
"lookup",
|
|
83
|
-
lookupTarget
|
|
84
|
-
], { cwd: CWD });
|
|
85
|
-
return true;
|
|
104
|
+
return (await $`deno task cli lookup ${lookupTarget} -p`.stdin("null").stdout("piped").stderr("piped").noThrow().spawn()).stdout.includes(`id: URL '${lookupTarget}',`);
|
|
86
105
|
} catch (error) {
|
|
87
106
|
if (error instanceof Error) printErrorMessage`${error.message}`;
|
|
88
107
|
}
|
|
89
108
|
return false;
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* Wait for the server to be ready by checking if it responds to requests.
|
|
93
|
-
*/
|
|
94
|
-
async function waitForServer(url, timeout) {
|
|
95
|
-
const startTime = Date.now();
|
|
96
|
-
while (Date.now() - startTime < timeout) {
|
|
97
|
-
try {
|
|
98
|
-
if ((await fetch(url, { signal: AbortSignal.timeout(1e3) })).ok) return true;
|
|
99
|
-
} catch {}
|
|
100
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
async function serverClosure(dir, cmd, defaultPort, callback) {
|
|
105
|
-
const devCommand = cmd.split(" ");
|
|
106
|
-
const serverProcess = spawn(devCommand[0], devCommand.slice(1), {
|
|
107
|
-
cwd: dir,
|
|
108
|
-
stdio: [
|
|
109
|
-
"ignore",
|
|
110
|
-
"pipe",
|
|
111
|
-
"pipe"
|
|
112
|
-
],
|
|
113
|
-
detached: true
|
|
114
|
-
});
|
|
115
|
-
const stdout = createWriteStream(join(dir, "out.txt"), { flags: "a" });
|
|
116
|
-
const stderr = createWriteStream(join(dir, "err.txt"), { flags: "a" });
|
|
117
|
-
serverProcess.stdout?.pipe(stdout);
|
|
118
|
-
serverProcess.stderr?.pipe(stderr);
|
|
119
|
-
try {
|
|
120
|
-
return await callback(await determinePort(serverProcess).catch((err) => {
|
|
121
|
-
printErrorMessage`Failed to determine server port: ${err.message}`;
|
|
122
|
-
printErrorMessage`Use default port ${String(defaultPort)} for lookup.`;
|
|
123
|
-
return defaultPort;
|
|
124
|
-
}));
|
|
125
|
-
} finally {
|
|
126
|
-
try {
|
|
127
|
-
process.kill(-serverProcess.pid, "SIGKILL");
|
|
128
|
-
} catch {
|
|
129
|
-
serverProcess.kill("SIGKILL");
|
|
130
|
-
stdout.end();
|
|
131
|
-
stderr.end();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function determinePort(server) {
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
const timeout = setTimeout(() => {
|
|
138
|
-
reject(/* @__PURE__ */ new Error("Timeout: Could not determine port from server output"));
|
|
139
|
-
}, STARTUP_TIMEOUT);
|
|
140
|
-
let stdoutData = "";
|
|
141
|
-
let stderrData = "";
|
|
142
|
-
const portPatterns = [
|
|
143
|
-
/listening on.*:(\d+)/i,
|
|
144
|
-
/server.*:(\d+)/i,
|
|
145
|
-
/port\s*:?\s*(\d+)/i,
|
|
146
|
-
/https?:\/\/localhost:(\d+)/i,
|
|
147
|
-
/https?:\/\/0\.0\.0\.0:(\d+)/i,
|
|
148
|
-
/https?:\/\/127\.0\.0\.1:(\d+)/i,
|
|
149
|
-
/https?:\/\/[^:]+:(\d+)/i
|
|
150
|
-
];
|
|
151
|
-
const checkForPort = (data) => {
|
|
152
|
-
for (const pattern of portPatterns) {
|
|
153
|
-
const match = data.match(pattern);
|
|
154
|
-
if (match && match[1]) {
|
|
155
|
-
const port = Number.parseInt(match[1], 10);
|
|
156
|
-
if (port > 0 && port < 65536) {
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
return port;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
};
|
|
164
|
-
server.stdout.on("data", (chunk) => {
|
|
165
|
-
stdoutData += chunk.toString();
|
|
166
|
-
const port = checkForPort(stdoutData);
|
|
167
|
-
if (port) resolve(port);
|
|
168
|
-
});
|
|
169
|
-
server.stderr.on("data", (chunk) => {
|
|
170
|
-
stderrData += chunk.toString();
|
|
171
|
-
const port = checkForPort(stderrData);
|
|
172
|
-
if (port) resolve(port);
|
|
173
|
-
});
|
|
174
|
-
server.on("error", (err) => {
|
|
175
|
-
clearTimeout(timeout);
|
|
176
|
-
reject(err);
|
|
177
|
-
});
|
|
178
|
-
server.on("exit", (code) => {
|
|
179
|
-
clearTimeout(timeout);
|
|
180
|
-
reject(/* @__PURE__ */ new Error(`Server exited with code ${code} before port could be determined`));
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
109
|
}
|
|
184
110
|
//#endregion
|
|
185
111
|
export { runServerAndLookupUser as default };
|
package/dist/test/mod.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { testInitCommand } from "../command.js";
|
|
2
2
|
import runTestInit from "./action.js";
|
|
3
|
+
import process from "node:process";
|
|
3
4
|
import { run } from "@optique/run";
|
|
4
5
|
//#region src/test/mod.ts
|
|
5
6
|
async function main() {
|
|
6
|
-
console.log("Running test-init command...");
|
|
7
7
|
await runTestInit(run(testInitCommand, {
|
|
8
8
|
programName: "fedify-test-init",
|
|
9
9
|
help: "both"
|
|
10
10
|
}));
|
|
11
|
+
process.exit(0);
|
|
11
12
|
}
|
|
12
13
|
await main();
|
|
13
14
|
//#endregion
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { printErrorMessage, printMessage } from "../utils.js";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { createConnection, createServer } from "node:net";
|
|
7
|
+
//#region src/test/port.ts
|
|
8
|
+
/**
|
|
9
|
+
* Check if a port is currently in use by attempting a TCP connection.
|
|
10
|
+
*/
|
|
11
|
+
function isPortInUse(port, timeout = 1e3) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const socket = createConnection({
|
|
14
|
+
port,
|
|
15
|
+
host: "localhost"
|
|
16
|
+
});
|
|
17
|
+
socket.setTimeout(timeout);
|
|
18
|
+
socket.once("connect", () => {
|
|
19
|
+
socket.destroy();
|
|
20
|
+
resolve(true);
|
|
21
|
+
});
|
|
22
|
+
socket.once("timeout", () => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
socket.once("error", () => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Poll until the port is free or timeout is reached.
|
|
34
|
+
* Returns true if the port was released, false if still occupied.
|
|
35
|
+
*/
|
|
36
|
+
async function waitForPortRelease(port, timeout = 5e3) {
|
|
37
|
+
const start = Date.now();
|
|
38
|
+
while (Date.now() - start < timeout) {
|
|
39
|
+
if (!await isPortInUse(port)) return true;
|
|
40
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Attempt to kill any process listening on the given port using lsof.
|
|
46
|
+
*/
|
|
47
|
+
async function killProcessOnPort(port) {
|
|
48
|
+
try {
|
|
49
|
+
const pids = await new Promise((resolve, reject) => {
|
|
50
|
+
execFile("lsof", [
|
|
51
|
+
"-t",
|
|
52
|
+
`-i:${port}`,
|
|
53
|
+
"-sTCP:LISTEN"
|
|
54
|
+
], (err, stdout) => {
|
|
55
|
+
if (err) reject(err);
|
|
56
|
+
else resolve(stdout);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
for (const pid of pids.trim().split("\n").filter(Boolean)) try {
|
|
60
|
+
process.kill(parseInt(pid, 10), "SIGKILL");
|
|
61
|
+
} catch {}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reserve a free port by binding to port 0 and letting the OS assign one.
|
|
66
|
+
* The socket is held until the returned `release` function is called,
|
|
67
|
+
* eliminating the race window between discovery and actual use.
|
|
68
|
+
*/
|
|
69
|
+
function reservePort() {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const server = createServer();
|
|
72
|
+
server.listen(0, () => {
|
|
73
|
+
const addr = server.address();
|
|
74
|
+
if (addr == null || typeof addr === "string") {
|
|
75
|
+
server.close();
|
|
76
|
+
reject(/* @__PURE__ */ new Error("Failed to get port from server"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
resolve({
|
|
80
|
+
port: addr.port,
|
|
81
|
+
release: () => new Promise((r) => server.close(() => r()))
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
server.on("error", reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const ENTRY_FILES = {
|
|
88
|
+
"bare-bones": "src/main.ts",
|
|
89
|
+
express: "src/index.ts",
|
|
90
|
+
hono: "src/index.ts",
|
|
91
|
+
elysia: "src/index.ts"
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Replace the hardcoded default port with `newPort` in the generated test
|
|
95
|
+
* app's source files. Strategy varies by framework.
|
|
96
|
+
*/
|
|
97
|
+
async function replacePortInApp(dir, wf, defaultPort, newPort) {
|
|
98
|
+
if (defaultPort === newPort) return;
|
|
99
|
+
const entryFile = ENTRY_FILES[wf];
|
|
100
|
+
if (entryFile) {
|
|
101
|
+
const filePath = join(dir, entryFile);
|
|
102
|
+
await writeFile(filePath, (await readFile(filePath, "utf8")).replaceAll(String(defaultPort), String(newPort)));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (wf === "nitro") {
|
|
106
|
+
await appendFile(join(dir, ".env"), `\nPORT=${newPort}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (wf === "astro") {
|
|
110
|
+
const configPath = join(dir, "astro.config.ts");
|
|
111
|
+
await writeFile(configPath, (await readFile(configPath, "utf8")).replace("defineConfig({", `defineConfig({\n server: { port: ${newPort} },`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
printErrorMessage`Unknown framework ${wf} — cannot replace port.`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Ensure a port is fully released after killing a server process.
|
|
118
|
+
* If the port is still occupied after waiting, force-kill the holder.
|
|
119
|
+
*/
|
|
120
|
+
async function ensurePortReleased(port) {
|
|
121
|
+
if (!await waitForPortRelease(port, 5e3)) {
|
|
122
|
+
printMessage` Port ${String(port)} still in use — force-killing...`;
|
|
123
|
+
await killProcessOnPort(port);
|
|
124
|
+
await waitForPortRelease(port, 3e3);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { ensurePortReleased, killProcessOnPort, replacePortInApp, reservePort };
|
package/dist/test/run.js
CHANGED
|
@@ -2,8 +2,8 @@ import { printMessage } from "../utils.js";
|
|
|
2
2
|
import createTestApp, { filterOptions, generateTestCases } from "./create.js";
|
|
3
3
|
import runServerAndLookupUser from "./lookup.js";
|
|
4
4
|
import { always, filter, map, pipe, tap, unless } from "@fxts/core";
|
|
5
|
-
import { optionNames } from "@optique/core";
|
|
6
5
|
import { join as join$1 } from "node:path";
|
|
6
|
+
import { optionNames } from "@optique/core";
|
|
7
7
|
//#region src/test/run.ts
|
|
8
8
|
const runTests = (dry) => ({ testDirPrefix, dryRun, hydRun, ...options }) => pipe(options, printStartMessage(dry), generateTestCases, filter(filterOptions), map(createTestApp(join$1(testDirPrefix, getMid(dryRun, hydRun, dry)), dry)), Array.fromAsync, unless(always(dry), runServerAndLookupUser));
|
|
9
9
|
const printStartMessage = (dry) => tap(() => printMessage`\n
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { printErrorMessage } from "../utils.js";
|
|
2
|
+
import { ensurePortReleased, killProcessOnPort } from "./port.js";
|
|
3
|
+
import $ from "@david/dax";
|
|
4
|
+
import { createWriteStream } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
//#region src/test/server.ts
|
|
7
|
+
const STARTUP_TIMEOUT = 1e4;
|
|
8
|
+
/**
|
|
9
|
+
* Wait for the server to be ready by checking if it responds to requests.
|
|
10
|
+
*/
|
|
11
|
+
async function waitForServer(url, timeout = STARTUP_TIMEOUT) {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
while (Date.now() - startTime < timeout) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
16
|
+
const ok = response.ok;
|
|
17
|
+
await response.body?.cancel();
|
|
18
|
+
if (ok) return true;
|
|
19
|
+
} catch {}
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
async function serverClosure(dir, cmd, defaultPort, callback, releasePort) {
|
|
25
|
+
await releasePort?.();
|
|
26
|
+
const serverProcess = $`${cmd.split(" ")}`.cwd(dir).env("PORT", String(defaultPort)).stdin("null").stdout("piped").stderr("piped").noThrow().spawn();
|
|
27
|
+
serverProcess.catch(() => {});
|
|
28
|
+
const [stdoutForFile, stdoutForPort] = serverProcess.stdout().tee();
|
|
29
|
+
const [stderrForFile, stderrForPort] = serverProcess.stderr().tee();
|
|
30
|
+
const cleanup = new AbortController();
|
|
31
|
+
const outFile = createWriteStream(join(dir, "out.txt"), { flags: "a" });
|
|
32
|
+
const errFile = createWriteStream(join(dir, "err.txt"), { flags: "a" });
|
|
33
|
+
const pipeOutDone = pipeStream(stdoutForFile, outFile, cleanup.signal);
|
|
34
|
+
const pipeErrDone = pipeStream(stderrForFile, errFile, cleanup.signal);
|
|
35
|
+
let port = defaultPort;
|
|
36
|
+
try {
|
|
37
|
+
port = await determinePort(stdoutForPort, stderrForPort, cleanup.signal).catch((err) => {
|
|
38
|
+
printErrorMessage`Failed to determine server port: ${err.message}`;
|
|
39
|
+
printErrorMessage`Use default port ${String(defaultPort)} for lookup.`;
|
|
40
|
+
return defaultPort;
|
|
41
|
+
});
|
|
42
|
+
return await callback(port);
|
|
43
|
+
} finally {
|
|
44
|
+
try {
|
|
45
|
+
serverProcess.kill("SIGKILL");
|
|
46
|
+
} catch {}
|
|
47
|
+
cleanup.abort();
|
|
48
|
+
await Promise.all([pipeOutDone, pipeErrDone]).catch(() => {});
|
|
49
|
+
await killProcessOnPort(port);
|
|
50
|
+
outFile.end();
|
|
51
|
+
errFile.end();
|
|
52
|
+
await ensurePortReleased(port);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function determinePort(stdout, stderr, signal) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
reject(/* @__PURE__ */ new Error("Timeout: Could not determine port from server output"));
|
|
59
|
+
}, STARTUP_TIMEOUT);
|
|
60
|
+
let stdoutData = "";
|
|
61
|
+
let stderrData = "";
|
|
62
|
+
let streamsEnded = 0;
|
|
63
|
+
const portPatterns = [
|
|
64
|
+
/listening on.*:(\d+)/i,
|
|
65
|
+
/server.*:(\d+)/i,
|
|
66
|
+
/port\s*:?\s*(\d+)/i,
|
|
67
|
+
/https?:\/\/localhost:(\d+)/i,
|
|
68
|
+
/https?:\/\/0\.0\.0\.0:(\d+)/i,
|
|
69
|
+
/https?:\/\/127\.0\.0\.1:(\d+)/i,
|
|
70
|
+
/https?:\/\/[^:]+:(\d+)/i
|
|
71
|
+
];
|
|
72
|
+
const checkForPort = (data) => {
|
|
73
|
+
for (const pattern of portPatterns) {
|
|
74
|
+
const match = data.match(pattern);
|
|
75
|
+
if (match && match[1]) {
|
|
76
|
+
const port = Number.parseInt(match[1], 10);
|
|
77
|
+
if (port > 0 && port < 65536) {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
return port;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const onStreamEnd = () => {
|
|
86
|
+
streamsEnded++;
|
|
87
|
+
if (streamsEnded === 2) {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
reject(/* @__PURE__ */ new Error("Server exited before port could be determined"));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const readStream = async (stream, onData) => {
|
|
93
|
+
const reader = stream.getReader();
|
|
94
|
+
const onAbort = () => void reader.cancel().catch(() => {});
|
|
95
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
96
|
+
const decoder = new TextDecoder();
|
|
97
|
+
try {
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done) break;
|
|
101
|
+
onData(decoder.decode(value, { stream: true }));
|
|
102
|
+
}
|
|
103
|
+
} catch {} finally {
|
|
104
|
+
signal?.removeEventListener("abort", onAbort);
|
|
105
|
+
reader.releaseLock();
|
|
106
|
+
onStreamEnd();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
readStream(stdout, (chunk) => {
|
|
110
|
+
stdoutData += chunk;
|
|
111
|
+
const port = checkForPort(stdoutData);
|
|
112
|
+
if (port) resolve(port);
|
|
113
|
+
});
|
|
114
|
+
readStream(stderr, (chunk) => {
|
|
115
|
+
stderrData += chunk;
|
|
116
|
+
const port = checkForPort(stderrData);
|
|
117
|
+
if (port) resolve(port);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function pipeStream(readable, writable, signal) {
|
|
122
|
+
const reader = readable.getReader();
|
|
123
|
+
const onAbort = () => void reader.cancel().catch(() => {});
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
try {
|
|
126
|
+
while (true) {
|
|
127
|
+
const { done, value } = await reader.read();
|
|
128
|
+
if (done) break;
|
|
129
|
+
writable.write(value);
|
|
130
|
+
}
|
|
131
|
+
} catch {} finally {
|
|
132
|
+
signal?.removeEventListener("abort", onAbort);
|
|
133
|
+
reader.releaseLock();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { STARTUP_TIMEOUT, serverClosure, waitForServer };
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { message } from "@optique/core";
|
|
2
1
|
import { toMerged } from "es-toolkit";
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { message } from "@optique/core";
|
|
5
3
|
//#region src/utils.d.ts
|
|
6
4
|
/** Makes all properties of `T` required and non-nullable. */
|
|
7
5
|
type RequiredNotNull<T> = { [P in keyof T]: NonNullable<T[P]> };
|
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { isObject } from "@fxts/core";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { print, printError } from "@optique/run";
|
|
4
|
+
import { flow, toMerged } from "es-toolkit";
|
|
4
5
|
import { message } from "@optique/core";
|
|
5
6
|
import { Chalk } from "chalk";
|
|
6
|
-
import { flow, toMerged } from "es-toolkit";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
7
|
/** Chalk instance configured based on {@link colorEnabled}. */
|
|
9
8
|
const colors = new Chalk(process.stdout.isTTY && !("NO_COLOR" in process.env && process.env.NO_COLOR !== "") ? {} : { level: 0 });
|
|
10
9
|
/** Type guard that checks whether a value is a `Promise`. */
|
|
@@ -53,85 +52,10 @@ const formatJson = (obj) => JSON.stringify(obj, null, 2) + "\n";
|
|
|
53
52
|
const notEmpty = (s) => s.length > 0;
|
|
54
53
|
/** Type guard that checks whether an error is a "file not found" (`ENOENT`) error. */
|
|
55
54
|
const isNotFoundError = (e) => isObject(e) && "code" in e && e.code === "ENOENT";
|
|
56
|
-
/**
|
|
57
|
-
* Error thrown when a spawned shell command exits with a non-zero code.
|
|
58
|
-
* Captures stdout, stderr, exit code, and the original command array.
|
|
59
|
-
*/
|
|
60
|
-
var CommandError = class extends Error {
|
|
61
|
-
commandLine;
|
|
62
|
-
constructor(message, stdout, stderr, code, command) {
|
|
63
|
-
super(message);
|
|
64
|
-
this.stdout = stdout;
|
|
65
|
-
this.stderr = stderr;
|
|
66
|
-
this.code = code;
|
|
67
|
-
this.command = command;
|
|
68
|
-
this.name = "CommandError";
|
|
69
|
-
this.commandLine = command.join(" ");
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Executes a shell command (or a chain of commands joined by `"&&"`) as child
|
|
74
|
-
* processes and returns the combined stdout/stderr output.
|
|
75
|
-
* Throws a {@link CommandError} if any command in the chain exits with a
|
|
76
|
-
* non-zero code.
|
|
77
|
-
*
|
|
78
|
-
* @param command - The command as an array of strings; use `"&&"` to chain
|
|
79
|
-
* @param options - Options forwarded to `node:child_process.spawn`
|
|
80
|
-
* @returns A promise resolving to `{ stdout, stderr }`
|
|
81
|
-
*/
|
|
82
|
-
const runSubCommand = async (command, options) => {
|
|
83
|
-
const commands = command.reduce((acc, cur) => {
|
|
84
|
-
if (cur === "&&") acc.push([]);
|
|
85
|
-
else {
|
|
86
|
-
if (acc.length === 0) acc.push([]);
|
|
87
|
-
acc[acc.length - 1].push(cur);
|
|
88
|
-
}
|
|
89
|
-
return acc;
|
|
90
|
-
}, []);
|
|
91
|
-
const results = {
|
|
92
|
-
stdout: "",
|
|
93
|
-
stderr: ""
|
|
94
|
-
};
|
|
95
|
-
for (const cmd of commands) try {
|
|
96
|
-
const result = await runSingularCommand(cmd, options);
|
|
97
|
-
results.stdout += (results.stdout ? "\n" : "") + result.stdout;
|
|
98
|
-
results.stderr += (results.stderr ? "\n" : "") + result.stderr;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (error instanceof CommandError) {
|
|
101
|
-
results.stdout += (results.stdout ? "\n" : "") + error.stdout;
|
|
102
|
-
results.stderr += (results.stderr ? "\n" : "") + error.stderr;
|
|
103
|
-
}
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
return results;
|
|
107
|
-
};
|
|
108
|
-
const runSingularCommand = (command, options) => new Promise((resolve, reject) => {
|
|
109
|
-
let stdout = "";
|
|
110
|
-
let stderr = "";
|
|
111
|
-
const child = spawn(command[0], command.slice(1), options);
|
|
112
|
-
child.stdout?.on("data", (data) => {
|
|
113
|
-
stdout += data.toString();
|
|
114
|
-
});
|
|
115
|
-
child.stderr?.on("data", (data) => {
|
|
116
|
-
stderr += data.toString();
|
|
117
|
-
});
|
|
118
|
-
child.on("close", (code) => {
|
|
119
|
-
if (code === 0) resolve({
|
|
120
|
-
stdout: stdout.trim(),
|
|
121
|
-
stderr: stderr.trim()
|
|
122
|
-
});
|
|
123
|
-
else reject(new CommandError(`Command exited with code ${code ?? "unknown"}`, stdout.trim(), stderr.trim(), code ?? -1, command));
|
|
124
|
-
});
|
|
125
|
-
child.on("error", (error) => {
|
|
126
|
-
reject(error);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
55
|
/** Returns the current working directory. */
|
|
130
56
|
const getCwd = () => process.cwd();
|
|
131
57
|
/** Returns the current OS platform (e.g., `"darwin"`, `"win32"`, `"linux"`). */
|
|
132
58
|
const getOsType = () => process.platform;
|
|
133
|
-
/** Exits the process with the given exit code. */
|
|
134
|
-
const exit = (code) => process.exit(code);
|
|
135
59
|
/**
|
|
136
60
|
* Generates the cartesian product of multiple iterables.
|
|
137
61
|
* Used by the test suite to enumerate all option combinations.
|
|
@@ -151,4 +75,4 @@ const printMessage = flow(message, print);
|
|
|
151
75
|
/** Prints a formatted error message to stderr using `@optique/run`'s `printError`. */
|
|
152
76
|
const printErrorMessage = flow(message, printError);
|
|
153
77
|
//#endregion
|
|
154
|
-
export {
|
|
78
|
+
export { colors, formatJson, getCwd, getOsType, isNotFoundError, merge$1 as merge, notEmpty, printErrorMessage, printMessage, product, replace, replaceAll, set };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PACKAGE_VERSION, readTemplate } from "../lib.js";
|
|
2
2
|
import { PACKAGE_MANAGER } from "../const.js";
|
|
3
|
+
import { npm__astrojs_node, npm__deno_astro_adapter, npm__types_node_22, npm_typescript } from "../json/deps.js";
|
|
3
4
|
import { defaultDenoDependencies, defaultDevDependencies } from "./const.js";
|
|
4
5
|
import { getInstruction } from "./utils.js";
|
|
5
6
|
//#region src/webframeworks/astro.ts
|
|
@@ -11,17 +12,17 @@ const astroDescription = {
|
|
|
11
12
|
command: Array.from(getAstroInitCommand(pm)),
|
|
12
13
|
dependencies: pm === "deno" ? {
|
|
13
14
|
...defaultDenoDependencies,
|
|
14
|
-
"@deno/astro-adapter":
|
|
15
|
+
"@deno/astro-adapter": `npm:@deno/astro-adapter@${npm__deno_astro_adapter}`,
|
|
15
16
|
"@fedify/astro": PACKAGE_VERSION
|
|
16
17
|
} : {
|
|
17
|
-
"@astrojs/node":
|
|
18
|
+
"@astrojs/node": npm__astrojs_node,
|
|
18
19
|
"@fedify/astro": PACKAGE_VERSION
|
|
19
20
|
},
|
|
20
21
|
devDependencies: {
|
|
21
22
|
...defaultDevDependencies,
|
|
22
23
|
...pm !== "deno" ? {
|
|
23
|
-
typescript:
|
|
24
|
-
"@types/node":
|
|
24
|
+
typescript: npm_typescript,
|
|
25
|
+
"@types/node": npm__types_node_22
|
|
25
26
|
} : {}
|
|
26
27
|
},
|
|
27
28
|
federationFile: "src/federation.ts",
|
|
@@ -65,6 +66,7 @@ function* getAstroInitCommand(pm) {
|
|
|
65
66
|
yield "--no-git";
|
|
66
67
|
yield "--skip-houston";
|
|
67
68
|
yield "-y";
|
|
69
|
+
if (pm !== "deno") yield "--no-install";
|
|
68
70
|
yield "&&";
|
|
69
71
|
yield "rm";
|
|
70
72
|
yield "astro.config.mjs";
|