@fedify/init 2.2.0-dev.628 → 2.2.0-dev.638
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.mjs → configs.js} +7 -7
- package/dist/action/{deps.mjs → deps.js} +7 -6
- package/dist/action/{env.mjs → env.js} +2 -2
- package/dist/action/{mod.d.mts → mod.d.ts} +2 -2
- package/dist/action/{mod.mjs → mod.js} +9 -11
- package/dist/action/{notice.mjs → notice.js} +2 -2
- package/dist/action/{patch.mjs → patch.js} +6 -6
- package/dist/action/{recommend.mjs → recommend.js} +4 -4
- package/dist/action/{set.mjs → set.js} +3 -3
- package/dist/action/{templates.mjs → templates.js} +2 -2
- package/dist/action/{utils.mjs → utils.js} +22 -1
- package/dist/ask/{dir.mjs → dir.js} +4 -3
- package/dist/ask/{kv.mjs → kv.js} +3 -3
- package/dist/ask/{mod.mjs → mod.js} +5 -5
- package/dist/ask/{mq.mjs → mq.js} +3 -3
- package/dist/ask/{pm.mjs → pm.js} +3 -3
- package/dist/ask/{wf.mjs → wf.js} +2 -2
- package/dist/{command.d.mts → command.d.ts} +3 -3
- package/dist/{command.mjs → command.js} +1 -1
- package/dist/{const.mjs → const.js} +4 -3
- package/dist/{deno.mjs → deno.js} +1 -1
- package/dist/json/deps.js +31 -0
- package/dist/json/deps.json +31 -0
- package/dist/json/{kv.mjs → kv.js} +3 -3
- package/dist/json/kv.json +63 -17
- package/dist/json/{mq.mjs → mq.js} +5 -5
- package/dist/json/mq.json +5 -5
- package/dist/{lib.mjs → lib.js} +10 -13
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +3 -0
- 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/{action.mjs → action.js} +5 -5
- package/dist/test/{create.mjs → create.js} +14 -28
- package/dist/test/{db.mjs → db.js} +3 -3
- package/dist/test/{fill.mjs → fill.js} +1 -1
- package/dist/test/lookup.js +111 -0
- package/dist/test/{mod.mjs → mod.js} +4 -3
- package/dist/test/port.js +128 -0
- package/dist/test/{run.mjs → run.js} +4 -4
- package/dist/test/server.js +137 -0
- package/dist/test/{utils.mjs → utils.js} +1 -1
- package/dist/{types.d.mts → types.d.ts} +3 -3
- package/dist/{utils.d.mts → utils.d.ts} +1 -3
- package/dist/{utils.mjs → utils.js} +2 -78
- package/dist/webframeworks/{astro.mjs → astro.js} +10 -8
- package/dist/webframeworks/{bare-bones.mjs → bare-bones.js} +13 -12
- package/dist/webframeworks/{const.mjs → const.js} +4 -2
- package/dist/webframeworks/{elysia.mjs → elysia.js} +15 -14
- package/dist/webframeworks/{express.mjs → express.js} +10 -9
- package/dist/webframeworks/{hono.mjs → hono.js} +17 -16
- package/dist/webframeworks/{mod.mjs → mod.js} +10 -8
- package/dist/webframeworks/{next.mjs → next.js} +6 -5
- package/dist/webframeworks/{nitro.mjs → nitro.js} +4 -4
- package/dist/webframeworks/solidstart.js +70 -0
- package/dist/webframeworks/{utils.mjs → utils.js} +1 -1
- package/package.json +4 -1
- package/dist/action/install.mjs +0 -18
- package/dist/action/precommand.mjs +0 -22
- package/dist/mod.d.mts +0 -3
- package/dist/mod.mjs +0 -3
- package/dist/test/lookup.mjs +0 -185
- /package/dist/action/{const.mjs → const.js} +0 -0
- /package/dist/action/{dir.mjs → dir.js} +0 -0
- /package/dist/{const.d.mts → const.d.ts} +0 -0
- /package/dist/json/{biome.mjs → biome.js} +0 -0
- /package/dist/json/{db-to-check.mjs → db-to-check.js} +0 -0
- /package/dist/json/{pm.mjs → pm.js} +0 -0
- /package/dist/json/{rt.mjs → rt.js} +0 -0
- /package/dist/json/{vscode-settings-for-deno.mjs → vscode-settings-for-deno.js} +0 -0
- /package/dist/json/{vscode-settings.mjs → vscode-settings.js} +0 -0
- /package/dist/test/{mod.d.mts → mod.d.ts} +0 -0
package/dist/{lib.mjs → lib.js}
RENAMED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
import { version } from "./deno.js";
|
|
2
|
+
import kv_default from "./json/kv.js";
|
|
3
|
+
import mq_default from "./json/mq.js";
|
|
4
|
+
import pm_default from "./json/pm.js";
|
|
5
|
+
import rt_default from "./json/rt.js";
|
|
6
|
+
import { isNotFoundError } from "./utils.js";
|
|
7
7
|
import { entries, evolve, fromEntries, isObject, map, negate, pipe, throwIf } from "@fxts/core";
|
|
8
8
|
import process from "node:process";
|
|
9
|
-
import
|
|
9
|
+
import $ from "@david/dax";
|
|
10
10
|
import { getLogger } from "@logtape/logtape";
|
|
11
|
+
import { toMerged } from "es-toolkit";
|
|
11
12
|
import { readFileSync } from "node:fs";
|
|
12
13
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
13
14
|
import { dirname, join as join$1 } from "node:path";
|
|
@@ -20,7 +21,7 @@ const logger = getLogger([
|
|
|
20
21
|
"cli",
|
|
21
22
|
"init"
|
|
22
23
|
]);
|
|
23
|
-
const addFedifyDeps = (json) => Object.fromEntries(Object.entries(json).map(([key, value]) => [key, toMerged(value, { dependencies: {
|
|
24
|
+
const addFedifyDeps = (json) => Object.fromEntries(Object.entries(json).map(([key, value]) => [key, toMerged(value, { dependencies: { ...!NO_INTEGRATIONS.includes(key) && { [`@fedify/${key}`]: PACKAGE_VERSION } } })]));
|
|
24
25
|
const NO_INTEGRATIONS = [
|
|
25
26
|
"in-memory",
|
|
26
27
|
"in-process",
|
|
@@ -88,11 +89,7 @@ const readTemplate = async (templatePath) => {
|
|
|
88
89
|
const getDevCommand = (pm) => pm === "deno" ? "deno task dev" : pm === "bun" ? "bun dev" : `${pm} run dev`;
|
|
89
90
|
async function isCommandAvailable({ checkCommand, outputPattern }) {
|
|
90
91
|
try {
|
|
91
|
-
const { stdout } = await
|
|
92
|
-
null,
|
|
93
|
-
"pipe",
|
|
94
|
-
null
|
|
95
|
-
] });
|
|
92
|
+
const { stdout } = await $`${checkCommand}`.stdout("piped").spawn();
|
|
96
93
|
logger.debug("The stdout of the command {command} is: {stdout}", {
|
|
97
94
|
command: checkCommand,
|
|
98
95
|
stdout
|
package/dist/mod.d.ts
ADDED
package/dist/mod.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Router } from "@solidjs/router";
|
|
2
|
+
import { FileRoutes } from "@solidjs/start/router";
|
|
3
|
+
import { Suspense } from "solid-js";
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
return (
|
|
7
|
+
<Router root={(props) => <Suspense>{props.children}</Suspense>}>
|
|
8
|
+
<FileRoutes />
|
|
9
|
+
</Router>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @refresh reload
|
|
2
|
+
import { createHandler, StartServer } from "@solidjs/start/server";
|
|
3
|
+
|
|
4
|
+
export default createHandler(() => (
|
|
5
|
+
<StartServer
|
|
6
|
+
document={({ assets, children, scripts }) => (
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8" />
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
11
|
+
{assets}
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app">{children}</div>
|
|
15
|
+
{scripts}
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { set } from "../utils.
|
|
2
|
-
import { checkRequiredDbs } from "./db.
|
|
3
|
-
import { fillEmptyOptions } from "./fill.
|
|
4
|
-
import runTests from "./run.
|
|
5
|
-
import { emptyTestDir, genRunId, genTestDirPrefix, logTestDir } from "./utils.
|
|
1
|
+
import { set } from "../utils.js";
|
|
2
|
+
import { checkRequiredDbs } from "./db.js";
|
|
3
|
+
import { fillEmptyOptions } from "./fill.js";
|
|
4
|
+
import runTests from "./run.js";
|
|
5
|
+
import { emptyTestDir, genRunId, genTestDirPrefix, logTestDir } from "./utils.js";
|
|
6
6
|
import { pipe, tap, when } from "@fxts/core";
|
|
7
7
|
//#region src/test/action.ts
|
|
8
8
|
const runTestInit = (options) => pipe(options, set("runId", genRunId), set("testDirPrefix", genTestDirPrefix), tap(emptyTestDir), fillEmptyOptions, tap(checkRequiredDbs), tap(logTestDir), tap(when(isDryRun, runTests(true))), tap(when(isHydRun, runTests(false))));
|
|
@@ -1,43 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { kvStores, messageQueues } from "../lib.
|
|
4
|
-
import webFrameworks from "../webframeworks/mod.
|
|
1
|
+
import pm_default from "../json/pm.js";
|
|
2
|
+
import { printErrorMessage, printMessage, product } from "../utils.js";
|
|
3
|
+
import { kvStores, messageQueues } from "../lib.js";
|
|
4
|
+
import webFrameworks from "../webframeworks/mod.js";
|
|
5
5
|
import { filter, isEmpty, pipe, toArray } from "@fxts/core";
|
|
6
6
|
import process from "node:process";
|
|
7
|
-
import
|
|
7
|
+
import $ from "@david/dax";
|
|
8
8
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
9
9
|
import { join as join$1, sep } from "node:path";
|
|
10
|
+
import { values } from "@optique/core";
|
|
10
11
|
//#region src/test/create.ts
|
|
11
12
|
const BANNED_PMS = ["bun", "yarn"];
|
|
12
13
|
const createTestApp = (testDirPrefix, dry) => async (options) => {
|
|
13
14
|
const testDir = join$1(testDirPrefix, ...options);
|
|
14
15
|
const vals = values(testDir.split(sep).slice(-4));
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
stdio: [
|
|
19
|
-
"ignore",
|
|
20
|
-
"pipe",
|
|
21
|
-
"pipe"
|
|
22
|
-
]
|
|
23
|
-
}));
|
|
16
|
+
const result = await $`${toArray(genInitCommand(testDir, dry, options))}`.cwd(join$1(import.meta.dirname, "..", "..")).stdin("null").stdout("piped").stderr("piped").noThrow().spawn();
|
|
17
|
+
await saveOutputs(testDir, result);
|
|
18
|
+
if (result.code === 0) {
|
|
24
19
|
printMessage` Pass: ${vals}`;
|
|
25
20
|
return testDir;
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error instanceof CommandError) await saveOutputs(testDir, {
|
|
28
|
-
stdout: error.stdout,
|
|
29
|
-
stderr: error.stderr
|
|
30
|
-
});
|
|
31
|
-
else await saveOutputs(testDir, {
|
|
32
|
-
stdout: "",
|
|
33
|
-
stderr: error instanceof Error ? error.message : String(error)
|
|
34
|
-
});
|
|
35
|
-
printMessage` Fail: ${vals}`;
|
|
36
|
-
printMessage` Check out these files for more details:
|
|
37
|
-
${join$1(testDir, "out.txt")} and
|
|
38
|
-
${join$1(testDir, "err.txt")}\n`;
|
|
39
|
-
return "";
|
|
40
21
|
}
|
|
22
|
+
printMessage` Fail: ${vals}`;
|
|
23
|
+
printMessage` Check out these files for more details: \
|
|
24
|
+
${join$1(testDir, "out.txt")} and \
|
|
25
|
+
${join$1(testDir, "err.txt")}\n`;
|
|
26
|
+
return "";
|
|
41
27
|
};
|
|
42
28
|
function* genInitCommand(testDir, dry, [webFramework, packageManager, kvStore, messageQueue]) {
|
|
43
29
|
yield "deno";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { printErrorMessage, printMessage } from "../utils.
|
|
2
|
-
import { DB_TO_CHECK } from "../const.
|
|
3
|
-
import db_to_check_default from "../json/db-to-check.
|
|
1
|
+
import { printErrorMessage, printMessage } from "../utils.js";
|
|
2
|
+
import { DB_TO_CHECK } from "../const.js";
|
|
3
|
+
import db_to_check_default from "../json/db-to-check.js";
|
|
4
4
|
import { concat, filter, pipe, toArray, uniq } from "@fxts/core";
|
|
5
5
|
import { createConnection } from "node:net";
|
|
6
6
|
//#region src/test/db.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KV_STORE, MESSAGE_QUEUE, PACKAGE_MANAGER, WEB_FRAMEWORK } from "../const.
|
|
1
|
+
import { KV_STORE, MESSAGE_QUEUE, PACKAGE_MANAGER, WEB_FRAMEWORK } from "../const.js";
|
|
2
2
|
import { isEmpty, omit, pipe } from "@fxts/core";
|
|
3
3
|
//#region src/test/fill.ts
|
|
4
4
|
const fillEmptyOptions = (options) => pipe(options, fillWebFramework, fillPackageManager, fillKVStore, fillMessageQueue, fillRunMode);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { printErrorMessage, printMessage } from "../utils.js";
|
|
2
|
+
import { getDevCommand, kvStores, messageQueues, packageManagers } from "../lib.js";
|
|
3
|
+
import webFrameworks from "../webframeworks/mod.js";
|
|
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";
|
|
9
|
+
import { values } from "@optique/core";
|
|
10
|
+
//#region src/test/lookup.ts
|
|
11
|
+
const HANDLE = "john";
|
|
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(","));
|
|
18
|
+
/**
|
|
19
|
+
* Run servers for all generated apps and test them with the lookup command.
|
|
20
|
+
*
|
|
21
|
+
* @param dirs - Array of paths to generated app directories
|
|
22
|
+
*/
|
|
23
|
+
async function runServerAndLookupUser(dirs) {
|
|
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);
|
|
30
|
+
const successCount = results.filter(Boolean).length;
|
|
31
|
+
const failCount = results.length - successCount;
|
|
32
|
+
printMessage`Lookup Test Results:
|
|
33
|
+
Total: ${String(results.length)}
|
|
34
|
+
Passed: ${String(successCount)}
|
|
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;
|
|
57
|
+
}
|
|
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
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run the dev server and test with lookup command.
|
|
68
|
+
*/
|
|
69
|
+
async function testApp(dir) {
|
|
70
|
+
const [wf, pm, kv, mq] = parseLookupCase(dir);
|
|
71
|
+
printMessage` Testing ${values([
|
|
72
|
+
wf,
|
|
73
|
+
pm,
|
|
74
|
+
kv,
|
|
75
|
+
mq
|
|
76
|
+
])}...`;
|
|
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);
|
|
82
|
+
printMessage` Lookup ${result ? "successful" : "failed"} for ${values([
|
|
83
|
+
wf,
|
|
84
|
+
pm,
|
|
85
|
+
kv,
|
|
86
|
+
mq
|
|
87
|
+
])}!`;
|
|
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`;
|
|
91
|
+
printMessage`\n`;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
async function sendLookup(port) {
|
|
95
|
+
const serverUrl = `http://localhost:${port}`;
|
|
96
|
+
const lookupTarget = `${serverUrl}/users/${HANDLE}`;
|
|
97
|
+
printMessage` Waiting for server to start at ${serverUrl}...`;
|
|
98
|
+
if (!await waitForServer(serverUrl)) {
|
|
99
|
+
printErrorMessage`Server did not start within ${String(STARTUP_TIMEOUT)}ms`;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
printMessage` Server is ready. Running lookup command...`;
|
|
103
|
+
try {
|
|
104
|
+
return (await $`deno task cli lookup ${lookupTarget} -p`.stdin("null").stdout("piped").stderr("piped").noThrow().spawn()).stdout.includes(`id: URL '${lookupTarget}',`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof Error) printErrorMessage`${error.message}`;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { runServerAndLookupUser as default };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { testInitCommand } from "../command.
|
|
2
|
-
import runTestInit from "./action.
|
|
1
|
+
import { testInitCommand } from "../command.js";
|
|
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 };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { printMessage } from "../utils.
|
|
2
|
-
import createTestApp, { filterOptions, generateTestCases } from "./create.
|
|
3
|
-
import runServerAndLookupUser from "./lookup.
|
|
1
|
+
import { printMessage } from "../utils.js";
|
|
2
|
+
import createTestApp, { filterOptions, generateTestCases } from "./create.js";
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { InitCommand } from "./command.
|
|
2
|
-
import { PACKAGE_MANAGER } from "./const.
|
|
3
|
-
import { RequiredNotNull } from "./utils.
|
|
1
|
+
import { InitCommand } from "./command.js";
|
|
2
|
+
import { PACKAGE_MANAGER } from "./const.js";
|
|
3
|
+
import { RequiredNotNull } from "./utils.js";
|
|
4
4
|
import { Message } from "@optique/core";
|
|
5
5
|
|
|
6
6
|
//#region src/types.d.ts
|
|
@@ -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]> };
|