@fedify/cli 2.0.0-pr.474.1879 → 2.0.0-pr.479.1900
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/deno.json +1 -1
- package/dist/deno.js +1 -1
- package/dist/imagerenderer.js +1 -1
- package/dist/inbox.js +2 -5
- package/dist/init/action/configs.js +22 -13
- package/dist/init/action/const.js +10 -0
- package/dist/init/action/deps.js +26 -28
- package/dist/init/action/install.js +11 -13
- package/dist/init/action/mod.js +1 -1
- package/dist/init/action/notice.js +12 -19
- package/dist/init/action/patch.js +33 -27
- package/dist/init/action/precommand.js +7 -2
- package/dist/init/action/recommend.js +1 -1
- package/dist/init/action/set.js +1 -1
- package/dist/init/action/templates.js +2 -1
- package/dist/init/ask/kv.js +24 -13
- package/dist/init/ask/mq.js +26 -13
- package/dist/init/command.js +20 -3
- package/dist/init/lib.js +20 -13
- package/dist/init/mod.js +2 -1
- package/dist/init/templates/nitro/.env.test.tpl +1 -0
- package/dist/init/templates/nitro/nitro.config.ts.tpl +12 -3
- package/dist/init/test/action.js +15 -0
- package/dist/init/test/create.js +100 -0
- package/dist/init/test/fill.js +26 -0
- package/dist/init/test/lookup.js +189 -0
- package/dist/init/test/run.js +26 -0
- package/dist/init/test/utils.js +17 -0
- package/dist/init/webframeworks.js +31 -28
- package/dist/mod.js +4 -2
- package/dist/nodeinfo.js +6 -6
- package/dist/utils.js +75 -8
- package/package.json +5 -5
- package/src/inbox.tsx +1 -15
- package/src/init/action/configs.ts +56 -13
- package/src/init/action/const.ts +9 -0
- package/src/init/action/deps.ts +98 -30
- package/src/init/action/install.ts +17 -52
- package/src/init/action/notice.ts +16 -14
- package/src/init/action/patch.ts +48 -27
- package/src/init/action/precommand.ts +9 -2
- package/src/init/action/set.ts +2 -15
- package/src/init/action/templates.ts +3 -2
- package/src/init/action/utils.ts +1 -1
- package/src/init/ask/kv.ts +64 -21
- package/src/init/ask/mq.ts +69 -20
- package/src/init/command.ts +39 -1
- package/src/init/lib.ts +31 -28
- package/src/init/mod.ts +2 -1
- package/src/init/templates/nitro/.env.test.tpl +1 -0
- package/src/init/templates/nitro/nitro.config.ts.tpl +12 -3
- package/src/init/test/action.ts +25 -0
- package/src/init/test/create.ts +137 -0
- package/src/init/test/fill.ts +61 -0
- package/src/init/test/lookup.ts +253 -0
- package/src/init/test/run.ts +42 -0
- package/src/init/test/types.ts +34 -0
- package/src/init/test/utils.ts +21 -0
- package/src/init/types.ts +4 -3
- package/src/init/webframeworks.ts +35 -18
- package/src/mod.ts +10 -1
- package/src/nodeinfo.ts +2 -1
- package/src/utils.ts +128 -10
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { pipe, tap, when } from "@fxts/core";
|
|
2
|
+
import { set } from "../../utils.ts";
|
|
3
|
+
import type { TestInitCommand } from "../command.ts";
|
|
4
|
+
import { fillEmptyOptions } from "./fill.ts";
|
|
5
|
+
import { isDryRun, isHydRun, runTests } from "./run.ts";
|
|
6
|
+
import {
|
|
7
|
+
emptyTestDir,
|
|
8
|
+
genRunId,
|
|
9
|
+
genTestDirPrefix,
|
|
10
|
+
logTestDir,
|
|
11
|
+
} from "./utils.ts";
|
|
12
|
+
|
|
13
|
+
const runTestInit = (options: TestInitCommand) =>
|
|
14
|
+
pipe(
|
|
15
|
+
options,
|
|
16
|
+
set("runId", genRunId),
|
|
17
|
+
set("testDirPrefix", genTestDirPrefix),
|
|
18
|
+
tap(emptyTestDir),
|
|
19
|
+
fillEmptyOptions,
|
|
20
|
+
tap(when(isHydRun, runTests(false))),
|
|
21
|
+
tap(when(isDryRun, runTests(true))),
|
|
22
|
+
tap(logTestDir),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default runTestInit;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { filter, isEmpty, pipe, toArray } from "@fxts/core";
|
|
2
|
+
import { values } from "@optique/core";
|
|
3
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { join, sep } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import {
|
|
7
|
+
CommandError,
|
|
8
|
+
type GeneratedType,
|
|
9
|
+
printErrorMessage,
|
|
10
|
+
printMessage,
|
|
11
|
+
product,
|
|
12
|
+
runSubCommand,
|
|
13
|
+
} from "../../utils.ts";
|
|
14
|
+
import packageManagers from "../json/pm.json" with { type: "json" };
|
|
15
|
+
import { kvStores, messageQueues } from "../lib.ts";
|
|
16
|
+
import type {
|
|
17
|
+
KvStore,
|
|
18
|
+
MessageQueue,
|
|
19
|
+
PackageManager,
|
|
20
|
+
WebFramework,
|
|
21
|
+
} from "../types.ts";
|
|
22
|
+
import webFrameworks from "../webframeworks.ts";
|
|
23
|
+
import type { InitTestData, MultipleOption } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
const BANNED_PMS: PackageManager[] = ["bun", "yarn"];
|
|
26
|
+
|
|
27
|
+
const createTestApp = (testDirPrefix: string, dry: boolean) =>
|
|
28
|
+
async (
|
|
29
|
+
options: GeneratedType<ReturnType<typeof generateTestCases>>,
|
|
30
|
+
): Promise<string> => {
|
|
31
|
+
const testDir = join(testDirPrefix, ...options);
|
|
32
|
+
const vals = values(testDir.split(sep).slice(-4));
|
|
33
|
+
try {
|
|
34
|
+
const result = await runSubCommand(
|
|
35
|
+
toArray(genInitCommand(testDir, dry, options)),
|
|
36
|
+
{
|
|
37
|
+
cwd: join(import.meta.dirname!, "../../.."),
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await saveOutputs(testDir, result);
|
|
43
|
+
printMessage` Pass: ${vals}`;
|
|
44
|
+
return testDir;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof CommandError) {
|
|
47
|
+
await saveOutputs(testDir, {
|
|
48
|
+
stdout: error.stdout,
|
|
49
|
+
stderr: error.stderr,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
const errorMessage = error instanceof Error
|
|
53
|
+
? error.message
|
|
54
|
+
: String(error);
|
|
55
|
+
await saveOutputs(testDir, { stdout: "", stderr: errorMessage });
|
|
56
|
+
}
|
|
57
|
+
printMessage` Fail: ${vals}`;
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default createTestApp;
|
|
63
|
+
|
|
64
|
+
function* genInitCommand(
|
|
65
|
+
testDir: string,
|
|
66
|
+
dry: boolean,
|
|
67
|
+
[webFramework, packageManager, kvStore, messageQueue]: //
|
|
68
|
+
GeneratedType<ReturnType<typeof generateTestCases>>,
|
|
69
|
+
) {
|
|
70
|
+
yield "deno";
|
|
71
|
+
yield "run";
|
|
72
|
+
yield "-A";
|
|
73
|
+
yield "src/mod.ts";
|
|
74
|
+
yield "init";
|
|
75
|
+
yield testDir;
|
|
76
|
+
yield "-w";
|
|
77
|
+
yield webFramework;
|
|
78
|
+
yield "-p";
|
|
79
|
+
yield packageManager;
|
|
80
|
+
yield "-k";
|
|
81
|
+
yield kvStore;
|
|
82
|
+
yield "-m";
|
|
83
|
+
yield messageQueue;
|
|
84
|
+
yield "--test-mode";
|
|
85
|
+
if (dry) yield "-d";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const generateTestCases = <T extends Pick<InitTestData, MultipleOption>>(
|
|
89
|
+
{ webFramework, packageManager, kvStore, messageQueue }: T,
|
|
90
|
+
) => {
|
|
91
|
+
const pms = filterPackageManager(packageManager);
|
|
92
|
+
exitIfCasesEmpty([webFramework, pms, kvStore, messageQueue]);
|
|
93
|
+
|
|
94
|
+
return product(webFramework, pms, kvStore, messageQueue);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const filterPackageManager = (pm: PackageManager[]) =>
|
|
98
|
+
pipe(
|
|
99
|
+
pm,
|
|
100
|
+
filter(
|
|
101
|
+
(pm) =>
|
|
102
|
+
BANNED_PMS.includes(pm)
|
|
103
|
+
? printErrorMessage`${packageManagers[pm]["label"]} is not \
|
|
104
|
+
supported in test mode yet because ${packageManagers[pm]["label"]} don't \
|
|
105
|
+
support local file dependencies properly.`
|
|
106
|
+
: true,
|
|
107
|
+
),
|
|
108
|
+
toArray,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const exitIfCasesEmpty = (cases: string[][]): never | void => {
|
|
112
|
+
if (cases.some(isEmpty)) {
|
|
113
|
+
printErrorMessage`No test cases to run. Exiting.`;
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const saveOutputs = async (
|
|
119
|
+
dirPath: string,
|
|
120
|
+
{ stdout, stderr }: { stdout: string; stderr: string },
|
|
121
|
+
): Promise<void> => {
|
|
122
|
+
await mkdir(dirPath, { recursive: true });
|
|
123
|
+
if (stdout) await appendFile(join(dirPath, "out.txt"), stdout + "\n", "utf8");
|
|
124
|
+
if (stderr) await appendFile(join(dirPath, "err.txt"), stderr + "\n", "utf8");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function filterOptions(
|
|
128
|
+
options: GeneratedType<ReturnType<typeof generateTestCases>>,
|
|
129
|
+
): boolean {
|
|
130
|
+
const [wf, pm, kv, mq] = options as //
|
|
131
|
+
[WebFramework, PackageManager, KvStore, MessageQueue];
|
|
132
|
+
return [
|
|
133
|
+
webFrameworks[wf].packageManagers,
|
|
134
|
+
kvStores[kv].packageManagers,
|
|
135
|
+
messageQueues[mq].packageManagers,
|
|
136
|
+
].every((pms) => pms.includes(pm));
|
|
137
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isEmpty, pipe } from "@fxts/core";
|
|
2
|
+
import type { TestInitCommand } from "../command.ts";
|
|
3
|
+
import {
|
|
4
|
+
KV_STORE,
|
|
5
|
+
MESSAGE_QUEUE,
|
|
6
|
+
PACKAGE_MANAGER,
|
|
7
|
+
WEB_FRAMEWORK,
|
|
8
|
+
} from "../const.ts";
|
|
9
|
+
import type {
|
|
10
|
+
DefineAllOptions,
|
|
11
|
+
DefineOption,
|
|
12
|
+
MultipleOption,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export const fillEmptyOptions = <T extends TestInitCommand>(
|
|
16
|
+
options: T,
|
|
17
|
+
): DefineAllOptions<T> =>
|
|
18
|
+
pipe(
|
|
19
|
+
options,
|
|
20
|
+
fillWebFramework,
|
|
21
|
+
fillPackageManager,
|
|
22
|
+
fillKVStore,
|
|
23
|
+
fillMessageQueue,
|
|
24
|
+
fillRunMode,
|
|
25
|
+
) as DefineAllOptions<T>;
|
|
26
|
+
|
|
27
|
+
const fillOption = <
|
|
28
|
+
K extends MultipleOption,
|
|
29
|
+
AllValues = TestInitCommand[K] extends readonly (infer U)[] ? (U & string)[]
|
|
30
|
+
: never,
|
|
31
|
+
>(
|
|
32
|
+
key: K,
|
|
33
|
+
allValues: AllValues,
|
|
34
|
+
) =>
|
|
35
|
+
<T extends TestInitCommand>(
|
|
36
|
+
options: T,
|
|
37
|
+
): T extends Awaited<DefineOption<T, infer J>> | DefineOption<T, infer J>
|
|
38
|
+
? DefineOption<T, J | K>
|
|
39
|
+
: DefineOption<T, K> =>
|
|
40
|
+
({
|
|
41
|
+
...options,
|
|
42
|
+
[key]: (isEmpty(options[key])
|
|
43
|
+
? allValues
|
|
44
|
+
: (options[key].filter((i) =>
|
|
45
|
+
(allValues as readonly unknown[]).includes(i)
|
|
46
|
+
) as T[K])),
|
|
47
|
+
}) as T extends Awaited<DefineOption<T, infer J>> | DefineOption<T, infer J>
|
|
48
|
+
? DefineOption<T, J | K>
|
|
49
|
+
: DefineOption<T, K>;
|
|
50
|
+
|
|
51
|
+
const fillWebFramework = fillOption("webFramework", WEB_FRAMEWORK);
|
|
52
|
+
const fillPackageManager = fillOption("packageManager", PACKAGE_MANAGER);
|
|
53
|
+
const fillKVStore = fillOption("kvStore", KV_STORE);
|
|
54
|
+
const fillMessageQueue = fillOption("messageQueue", MESSAGE_QUEUE);
|
|
55
|
+
|
|
56
|
+
const fillRunMode = <T extends TestInitCommand>(
|
|
57
|
+
options: DefineAllOptions<T>,
|
|
58
|
+
): DefineAllOptions<T> => ({
|
|
59
|
+
...options,
|
|
60
|
+
...(options.hydRun || options.dryRun ? {} : { hydRun: true, dryRun: true }),
|
|
61
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { values } from "@optique/core";
|
|
2
|
+
import type { ChildProcessByStdio } from "node:child_process";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createWriteStream } from "node:fs";
|
|
5
|
+
import { join, sep } from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import type Stream from "node:stream";
|
|
8
|
+
import { printErrorMessage, printMessage, runSubCommand } from "../../utils.ts";
|
|
9
|
+
import { getDevCommand } from "../lib.ts";
|
|
10
|
+
import type {
|
|
11
|
+
KvStore,
|
|
12
|
+
MessageQueue,
|
|
13
|
+
PackageManager,
|
|
14
|
+
WebFramework,
|
|
15
|
+
} from "../types.ts";
|
|
16
|
+
import webFrameworks from "../webframeworks.ts";
|
|
17
|
+
|
|
18
|
+
const HANDLE = "john";
|
|
19
|
+
const STARTUP_TIMEOUT = 30000; // 30 seconds
|
|
20
|
+
const CWD = process.cwd();
|
|
21
|
+
const BANNED_WFS: WebFramework[] = ["next"];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run servers for all generated apps and test them with the lookup command.
|
|
25
|
+
*
|
|
26
|
+
* @param dirs - Array of paths to generated app directories
|
|
27
|
+
*/
|
|
28
|
+
export default async function runServerAndReadUser(
|
|
29
|
+
dirs: string[],
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const valid = dirs.filter(Boolean);
|
|
32
|
+
if (valid.length === 0) {
|
|
33
|
+
printErrorMessage`\nNo directories to lookup test.`;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const filtered = filterWebFrameworks(valid);
|
|
37
|
+
|
|
38
|
+
printMessage`\nLookup Test start for ${String(filtered.length)} app(s)!`;
|
|
39
|
+
|
|
40
|
+
const results = await Array.fromAsync(filtered, testApp);
|
|
41
|
+
|
|
42
|
+
const successCount = results.filter(Boolean).length;
|
|
43
|
+
const failCount = results.length - successCount;
|
|
44
|
+
|
|
45
|
+
printMessage`Lookup Test Results:
|
|
46
|
+
Total: ${String(results.length)}
|
|
47
|
+
Passed: ${String(successCount)}
|
|
48
|
+
Failed: ${String(failCount)}\n\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function filterWebFrameworks(
|
|
52
|
+
dirs: string[],
|
|
53
|
+
): string[] {
|
|
54
|
+
const wfs = new Set<WebFramework>(
|
|
55
|
+
dirs.map((dir) => dir.split(sep).slice(-4, -3)[0] as WebFramework),
|
|
56
|
+
);
|
|
57
|
+
const hasBanned = BANNED_WFS.filter((wf) => wfs.has(wf));
|
|
58
|
+
if (!hasBanned) {
|
|
59
|
+
return dirs;
|
|
60
|
+
}
|
|
61
|
+
const bannedLabels = hasBanned.map((wf) => webFrameworks[wf]["label"]);
|
|
62
|
+
printErrorMessage`\n${
|
|
63
|
+
values(bannedLabels)
|
|
64
|
+
} is not supported in test mode yet.`;
|
|
65
|
+
return dirs.filter((dir) =>
|
|
66
|
+
!BANNED_WFS.includes(dir.split(sep).slice(-4, -3)[0] as WebFramework)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run the dev server and test with lookup command.
|
|
72
|
+
*/
|
|
73
|
+
async function testApp(dir: string): Promise<boolean> {
|
|
74
|
+
const [wf, pm, kv, mq] = dir.split(sep).slice(-4) as //
|
|
75
|
+
[WebFramework, PackageManager, KvStore, MessageQueue];
|
|
76
|
+
|
|
77
|
+
printMessage` Testing ${values([wf, pm, kv, mq])}...`;
|
|
78
|
+
|
|
79
|
+
const result = await serverClosure(
|
|
80
|
+
dir,
|
|
81
|
+
getDevCommand(pm),
|
|
82
|
+
sendLookup,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
printMessage` Lookup ${result ? "successful" : "failed"} for ${
|
|
86
|
+
values([wf, pm, kv, mq])
|
|
87
|
+
}!`;
|
|
88
|
+
if (!result) {
|
|
89
|
+
printMessage` Check out these files for more details:
|
|
90
|
+
${join(dir, "out.txt")}
|
|
91
|
+
${join(dir, "err.txt")}\n`;
|
|
92
|
+
}
|
|
93
|
+
printMessage`\n`;
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sendLookup = async (port: number) => {
|
|
99
|
+
const serverUrl = `http://localhost:${port}`;
|
|
100
|
+
const lookupTarget = `${serverUrl}/users/${HANDLE}`;
|
|
101
|
+
// Wait for server to be ready
|
|
102
|
+
printMessage` Waiting for server to start at ${serverUrl}...`;
|
|
103
|
+
|
|
104
|
+
const isReady = await waitForServer(serverUrl, STARTUP_TIMEOUT);
|
|
105
|
+
|
|
106
|
+
if (!isReady) {
|
|
107
|
+
printErrorMessage`Server did not start within \
|
|
108
|
+
${String(STARTUP_TIMEOUT)}ms`;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
printMessage` Server is ready. Running lookup command...`;
|
|
113
|
+
|
|
114
|
+
// Run lookup command from original directory
|
|
115
|
+
try {
|
|
116
|
+
await runSubCommand(
|
|
117
|
+
["deno", "task", "cli", "lookup", lookupTarget],
|
|
118
|
+
{ cwd: CWD },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
printErrorMessage`${error.message}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wait for the server to be ready by checking if it responds to requests.
|
|
132
|
+
*/
|
|
133
|
+
async function waitForServer(url: string, timeout: number): Promise<boolean> {
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
|
|
136
|
+
while (Date.now() - startTime < timeout) {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
139
|
+
if (response.ok) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Server not ready yet, continue waiting
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wait 500ms before next attempt
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function serverClosure<T>(
|
|
154
|
+
dir: string,
|
|
155
|
+
cmd: string,
|
|
156
|
+
callback: (port: number) => Promise<T>,
|
|
157
|
+
): Promise<Awaited<T>> {
|
|
158
|
+
// Start the dev server using Node.js spawn
|
|
159
|
+
const devCommand = cmd.split(" ");
|
|
160
|
+
const serverProcess = spawn(devCommand[0], devCommand.slice(1), {
|
|
161
|
+
cwd: dir,
|
|
162
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
+
detached: true, // Create a new process group
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Append stdout and stderr to files
|
|
167
|
+
const stdout = createWriteStream(join(dir, "out.txt"), { flags: "a" });
|
|
168
|
+
const stderr = createWriteStream(join(dir, "err.txt"), { flags: "a" });
|
|
169
|
+
|
|
170
|
+
serverProcess.stdout?.pipe(stdout);
|
|
171
|
+
serverProcess.stderr?.pipe(stderr);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const port = await determinePort(serverProcess);
|
|
175
|
+
return await callback(port);
|
|
176
|
+
} finally {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(-serverProcess.pid!, "SIGKILL");
|
|
179
|
+
} catch {
|
|
180
|
+
serverProcess.kill("SIGKILL");
|
|
181
|
+
|
|
182
|
+
// Close file streams
|
|
183
|
+
stdout.end();
|
|
184
|
+
stderr.end();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function determinePort(
|
|
190
|
+
server: ChildProcessByStdio<null, Stream.Readable, Stream.Readable>,
|
|
191
|
+
): Promise<number> {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timeout = setTimeout(() => {
|
|
194
|
+
reject(
|
|
195
|
+
new Error("Timeout: Could not determine port from server output"),
|
|
196
|
+
);
|
|
197
|
+
}, STARTUP_TIMEOUT);
|
|
198
|
+
|
|
199
|
+
let stdoutData = "";
|
|
200
|
+
let stderrData = "";
|
|
201
|
+
|
|
202
|
+
// Common patterns for port detection
|
|
203
|
+
const portPatterns = [
|
|
204
|
+
/listening on.*:(\d+)/i,
|
|
205
|
+
/server.*:(\d+)/i,
|
|
206
|
+
/port\s*:?\s*(\d+)/i,
|
|
207
|
+
/localhost:(\d+)/i,
|
|
208
|
+
/0\.0\.0\.0:(\d+)/i,
|
|
209
|
+
/127\.0\.0\.1:(\d+)/i,
|
|
210
|
+
/https?:\/\/[^:]+:(\d+)/i,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const checkForPort = (data: string) => {
|
|
214
|
+
for (const pattern of portPatterns) {
|
|
215
|
+
const match = data.match(pattern);
|
|
216
|
+
if (match && match[1]) {
|
|
217
|
+
const port = Number.parseInt(match[1], 10);
|
|
218
|
+
if (port > 0 && port < 65536) {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
return port;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
server.stdout.on("data", (chunk) => {
|
|
228
|
+
stdoutData += chunk.toString();
|
|
229
|
+
const port = checkForPort(stdoutData);
|
|
230
|
+
if (port) resolve(port);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
server.stderr.on("data", (chunk) => {
|
|
234
|
+
stderrData += chunk.toString();
|
|
235
|
+
const port = checkForPort(stderrData);
|
|
236
|
+
if (port) resolve(port);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
server.on("error", (err) => {
|
|
240
|
+
clearTimeout(timeout);
|
|
241
|
+
reject(err);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
server.on("exit", (code) => {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
reject(
|
|
247
|
+
new Error(
|
|
248
|
+
`Server exited with code ${code} before port could be determined`,
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { filter, map, pipe, tap } from "@fxts/core";
|
|
2
|
+
import { optionNames } from "@optique/core";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { printMessage } from "../../utils.ts";
|
|
5
|
+
import createTestApp, { filterOptions, generateTestCases } from "./create.ts";
|
|
6
|
+
import runServerAndReadUser from "./lookup.ts";
|
|
7
|
+
import type { InitTestData } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export const isDryRun = <T extends { dryRun: boolean }>({ dryRun }: T) =>
|
|
10
|
+
dryRun;
|
|
11
|
+
export const isHydRun = <T extends { hydRun: boolean }>({ hydRun }: T) =>
|
|
12
|
+
hydRun;
|
|
13
|
+
|
|
14
|
+
export const runTests =
|
|
15
|
+
(dry: boolean) =>
|
|
16
|
+
<T extends InitTestData>({ testDirPrefix, dryRun, hydRun, ...options }: T) =>
|
|
17
|
+
pipe(
|
|
18
|
+
options,
|
|
19
|
+
printStartMessage,
|
|
20
|
+
generateTestCases,
|
|
21
|
+
filter(filterOptions),
|
|
22
|
+
map(createTestApp(join(testDirPrefix, getMid(dryRun, hydRun, dry)), dry)),
|
|
23
|
+
Array.fromAsync<string>,
|
|
24
|
+
runServerAndReadUser,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const printStartMessage: <T>(t: T) => T = tap(
|
|
28
|
+
() =>
|
|
29
|
+
printMessage`\n
|
|
30
|
+
Init Test start!
|
|
31
|
+
Options: ${
|
|
32
|
+
optionNames([
|
|
33
|
+
"Web Framework",
|
|
34
|
+
"Package Manager",
|
|
35
|
+
"KV Store",
|
|
36
|
+
"Message Queue",
|
|
37
|
+
])
|
|
38
|
+
}`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const getMid = (dryRun: boolean, hydRun: boolean, dry: boolean) =>
|
|
42
|
+
dryRun === hydRun ? dry ? "dry" : "hyd" : "";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TestInitCommand } from "../command.ts";
|
|
2
|
+
|
|
3
|
+
export interface InitTestData extends AllDefinedTestInitCommand {
|
|
4
|
+
runId: string;
|
|
5
|
+
testDirPrefix: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AllDefinedTestInitCommand extends TestInitCommand {
|
|
9
|
+
webFramework: (TestInitCommand["webFramework"][number] & string)[];
|
|
10
|
+
packageManager: (TestInitCommand["packageManager"][number] & string)[];
|
|
11
|
+
kvStore: (TestInitCommand["kvStore"][number] & string)[];
|
|
12
|
+
messageQueue: (TestInitCommand["messageQueue"][number] & string)[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type MultipleOption =
|
|
16
|
+
| "webFramework"
|
|
17
|
+
| "packageManager"
|
|
18
|
+
| "kvStore"
|
|
19
|
+
| "messageQueue";
|
|
20
|
+
|
|
21
|
+
export type DefineOption<T extends TestInitCommand, K extends MultipleOption> =
|
|
22
|
+
& Omit<T, K>
|
|
23
|
+
& {
|
|
24
|
+
[Key in MultipleOption]: Key extends K ? AllDefinedTestInitCommand[Key]
|
|
25
|
+
: T[Key];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type DefineAllOptions<T extends TestInitCommand> =
|
|
29
|
+
& Omit<T, MultipleOption>
|
|
30
|
+
& {
|
|
31
|
+
[K in MultipleOption]: TestInitCommand[K] extends readonly unknown[]
|
|
32
|
+
? AllDefinedTestInitCommand[K]
|
|
33
|
+
: never;
|
|
34
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { printMessage } from "../../utils.ts";
|
|
4
|
+
|
|
5
|
+
export const genRunId = () =>
|
|
6
|
+
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
7
|
+
|
|
8
|
+
export const genTestDirPrefix = <T extends { runId: string }>({ runId }: T) =>
|
|
9
|
+
join(tmpdir(), "fedify-init", runId);
|
|
10
|
+
|
|
11
|
+
export const emptyTestDir = <
|
|
12
|
+
T extends { testDirPrefix: string },
|
|
13
|
+
>({ testDirPrefix }: T) =>
|
|
14
|
+
Deno.remove(testDirPrefix, { recursive: true }).catch(() => {});
|
|
15
|
+
|
|
16
|
+
export const logTestDir = <
|
|
17
|
+
T extends { runId: string; testDirPrefix: string },
|
|
18
|
+
>({ runId, testDirPrefix }: T) =>
|
|
19
|
+
printMessage`Test run completed.
|
|
20
|
+
Run ID: ${runId}
|
|
21
|
+
Path: ${testDirPrefix}`;
|
package/src/init/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Message } from "@optique/core";
|
|
1
2
|
import type { RequiredNotNull } from "../utils.ts";
|
|
2
3
|
import type { InitCommand } from "./command.ts";
|
|
3
4
|
import type {
|
|
@@ -40,15 +41,15 @@ export interface WebFrameworkInitializer {
|
|
|
40
41
|
files?: Record<string, string>;
|
|
41
42
|
compilerOptions?: Record<string, string | boolean | number | string[] | null>;
|
|
42
43
|
tasks?: Record<string, string>;
|
|
43
|
-
instruction:
|
|
44
|
+
instruction: Message;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface WebFrameworkDescription {
|
|
47
48
|
label: string;
|
|
48
49
|
packageManagers: readonly PackageManager[];
|
|
50
|
+
defaultPort: number;
|
|
49
51
|
init(
|
|
50
|
-
projectName: string,
|
|
51
|
-
pm: PackageManager,
|
|
52
|
+
data: InitCommandOptions & { projectName: string },
|
|
52
53
|
): WebFrameworkInitializer;
|
|
53
54
|
}
|
|
54
55
|
|