qunitx-cli 0.19.1 → 0.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +592 -284
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -55,68 +55,67 @@ var init_kill_process_group = __esm({
|
|
|
55
55
|
|
|
56
56
|
// lib/utils/cleanup-browser-dir.ts
|
|
57
57
|
import fs from "node:fs/promises";
|
|
58
|
-
async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
async function processReferencesDir(entry, dirPath, dirName) {
|
|
59
|
+
try {
|
|
60
|
+
const [cwd, cmdline] = await Promise.all([
|
|
61
|
+
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
62
|
+
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
63
|
+
]);
|
|
64
|
+
if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
|
|
65
|
+
const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
|
|
66
|
+
const fdTargets = await Promise.all(
|
|
67
|
+
fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
|
|
68
|
+
);
|
|
69
|
+
return fdTargets.some((target) => target.startsWith(dirPath));
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
}
|
|
74
|
+
async function killAllReferencingProcesses(dirPath, dirName) {
|
|
66
75
|
const procEntries = await fs.readdir("/proc").catch(() => []);
|
|
67
76
|
await Promise.all(
|
|
68
77
|
procEntries.map(async (entry) => {
|
|
69
78
|
if (!/^\d+$/.test(entry)) return;
|
|
70
|
-
|
|
79
|
+
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
71
80
|
try {
|
|
72
|
-
|
|
73
|
-
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
74
|
-
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
75
|
-
]);
|
|
76
|
-
if (!cwd.startsWith(dirPath) && !cmdline.includes(dirName)) return;
|
|
77
|
-
try {
|
|
78
|
-
process.kill(pid, "SIGKILL");
|
|
79
|
-
killedPids.add(pid);
|
|
80
|
-
} catch {
|
|
81
|
-
}
|
|
81
|
+
process.kill(parseInt(entry), "SIGKILL");
|
|
82
82
|
} catch {
|
|
83
83
|
}
|
|
84
84
|
})
|
|
85
85
|
);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
killedPids.delete(pid);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
86
|
+
}
|
|
87
|
+
async function cleanupBrowserDir(dirPath) {
|
|
88
|
+
if (process.platform !== "linux") {
|
|
89
|
+
await fs.rm(dirPath, { recursive: true, force: true }).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
95
92
|
}
|
|
93
|
+
const dirName = dirPath.split("/").pop();
|
|
94
|
+
await killAllReferencingProcesses(dirPath, dirName);
|
|
96
95
|
const deadline = Date.now() + 5e3;
|
|
97
96
|
while (Date.now() < deadline) {
|
|
98
97
|
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(() => true).catch(() => false);
|
|
99
|
-
if (removed)
|
|
100
|
-
await
|
|
98
|
+
if (removed) return;
|
|
99
|
+
await killAllReferencingProcesses(dirPath, dirName);
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
101
101
|
}
|
|
102
|
-
if (await fs.access(dirPath).then(() => true).catch(() => false))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
if (!await fs.access(dirPath).then(() => true).catch(() => false))
|
|
103
|
+
return;
|
|
104
|
+
const diagEntries = await fs.readdir("/proc").catch(() => []);
|
|
105
|
+
await Promise.all(
|
|
106
|
+
diagEntries.map(async (entry) => {
|
|
107
|
+
if (!/^\d+$/.test(entry)) return;
|
|
108
|
+
try {
|
|
109
|
+
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
110
|
+
const cmdline = await fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "");
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`# [qunitx] cleanup failed: pid ${entry} still references ${dirPath} (cmdline: ${cmdline.replace(/\0/g, " ").slice(0, 120)})
|
|
113
113
|
`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
120
119
|
}
|
|
121
120
|
var init_cleanup_browser_dir = __esm({
|
|
122
121
|
"lib/utils/cleanup-browser-dir.ts"() {
|
|
@@ -168,36 +167,11 @@ async function preLaunchChrome(chromePath, args, headless = true) {
|
|
|
168
167
|
});
|
|
169
168
|
async function shutdown() {
|
|
170
169
|
proc.ref();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
proc.once("close", resolve);
|
|
177
|
-
});
|
|
178
|
-
if (proc.exitCode === null) killProcessGroup(proc.pid);
|
|
179
|
-
await closed;
|
|
180
|
-
await rm(userDataDir, { recursive: true, force: true }).catch(async () => {
|
|
181
|
-
const pgid = proc.pid;
|
|
182
|
-
const warnTimer = setTimeout(
|
|
183
|
-
() => process.stderr.write(
|
|
184
|
-
`# [qunitx] warning: Chrome process group ${pgid} still alive 500ms after SIGKILL, waiting...
|
|
185
|
-
`
|
|
186
|
-
),
|
|
187
|
-
500
|
|
188
|
-
);
|
|
189
|
-
warnTimer.unref();
|
|
190
|
-
while (true) {
|
|
191
|
-
try {
|
|
192
|
-
process.kill(-pgid, 0);
|
|
193
|
-
} catch {
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
197
|
-
}
|
|
198
|
-
clearTimeout(warnTimer);
|
|
199
|
-
await cleanupBrowserDir(userDataDir);
|
|
200
|
-
});
|
|
170
|
+
if (proc.exitCode === null) {
|
|
171
|
+
killProcessGroup(proc.pid);
|
|
172
|
+
await new Promise((resolve) => proc.once("close", resolve));
|
|
173
|
+
}
|
|
174
|
+
await cleanupBrowserDir(userDataDir);
|
|
201
175
|
}
|
|
202
176
|
}
|
|
203
177
|
var CDP_URL_REGEX;
|
|
@@ -385,21 +359,6 @@ var init_color = __esm({
|
|
|
385
359
|
}
|
|
386
360
|
});
|
|
387
361
|
|
|
388
|
-
// lib/utils/path-exists.ts
|
|
389
|
-
import fs2 from "node:fs/promises";
|
|
390
|
-
async function pathExists(path7) {
|
|
391
|
-
try {
|
|
392
|
-
await fs2.access(path7);
|
|
393
|
-
return true;
|
|
394
|
-
} catch {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
var init_path_exists = __esm({
|
|
399
|
-
"lib/utils/path-exists.ts"() {
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
362
|
// lib/utils/read-template.ts
|
|
404
363
|
import fs3 from "node:fs/promises";
|
|
405
364
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -795,11 +754,13 @@ var init_http = __esm({
|
|
|
795
754
|
if (!this.routes[method]) {
|
|
796
755
|
this.routes[method] = {};
|
|
797
756
|
}
|
|
757
|
+
const paramNames = this.#extractParamNames(path7);
|
|
798
758
|
this.routes[method][path7] = {
|
|
799
759
|
path: path7,
|
|
800
760
|
handler,
|
|
801
|
-
paramNames
|
|
802
|
-
isWildcard: path7 === "/*"
|
|
761
|
+
paramNames,
|
|
762
|
+
isWildcard: path7 === "/*",
|
|
763
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path7, paramNames)}$`) : null
|
|
803
764
|
};
|
|
804
765
|
}
|
|
805
766
|
#handleRequest(req, res) {
|
|
@@ -842,10 +803,8 @@ var init_http = __esm({
|
|
|
842
803
|
return false;
|
|
843
804
|
}
|
|
844
805
|
if (isWildcard || this.#matchPathSegments(path7, url)) {
|
|
845
|
-
if (route.
|
|
846
|
-
const
|
|
847
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
848
|
-
const regexMatches = regex.exec(url);
|
|
806
|
+
if (route.compiledRegex) {
|
|
807
|
+
const regexMatches = route.compiledRegex.exec(url);
|
|
849
808
|
if (regexMatches) {
|
|
850
809
|
route.paramValues = regexMatches.slice(1);
|
|
851
810
|
}
|
|
@@ -906,6 +865,21 @@ function setupWebServer(config, cachedContent) {
|
|
|
906
865
|
cachedContent.mainHTML.filePath,
|
|
907
866
|
config.projectRoot
|
|
908
867
|
);
|
|
868
|
+
const runtimeScript = testRuntimeToInject(config);
|
|
869
|
+
const mainIndexHTML = escapeAndInjectTestsToHTML(
|
|
870
|
+
mainHTMLWithReplacedAssets,
|
|
871
|
+
runtimeScript,
|
|
872
|
+
"./tests.js"
|
|
873
|
+
);
|
|
874
|
+
const mainQunitxHTML = escapeAndInjectTestsToHTML(
|
|
875
|
+
mainHTMLWithReplacedAssets,
|
|
876
|
+
runtimeScript,
|
|
877
|
+
"./filtered-tests.js"
|
|
878
|
+
);
|
|
879
|
+
const saveHTML = (filePath, html) => fsPromise.writeFile(filePath, html).catch(
|
|
880
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile ${filePath}: ${err.message}
|
|
881
|
+
`)
|
|
882
|
+
);
|
|
909
883
|
server.wss.on("connection", function connection(socket) {
|
|
910
884
|
socket.on("message", function message(data) {
|
|
911
885
|
const { event, details, qunitResult, abort } = JSON.parse(data);
|
|
@@ -915,17 +889,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
915
889
|
} else if (event === "connection") {
|
|
916
890
|
config._phase = "running";
|
|
917
891
|
if (!config._groupMode) process.stdout.write("TAP version 13\n");
|
|
918
|
-
if (config.debug && config._groupMode)
|
|
919
|
-
const allFiles = Object.keys(config.fsTree);
|
|
920
|
-
const relFiles = allFiles.map(
|
|
921
|
-
(filePath) => filePath.replace(`${config.projectRoot}/`, "")
|
|
922
|
-
);
|
|
923
|
-
const shown = relFiles.slice(0, 2);
|
|
924
|
-
const rest = relFiles.length - shown.length;
|
|
925
|
-
const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
|
|
926
|
-
process.stdout.write(`# ${blue(`\u2500\u2500 ${fileList} \u2500\u2500`)}
|
|
927
|
-
`);
|
|
928
|
-
}
|
|
892
|
+
if (config.debug && config._groupMode) debugGroupHeader(config);
|
|
929
893
|
config._resetTestTimeout?.();
|
|
930
894
|
} else if (event === "testEnd" && !abort) {
|
|
931
895
|
if (details.status === "failed") {
|
|
@@ -957,7 +921,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
957
921
|
});
|
|
958
922
|
server.get("/tests.js", (_req, res) => {
|
|
959
923
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
960
|
-
process.stdout.write(
|
|
924
|
+
config.debug && process.stdout.write(
|
|
961
925
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
962
926
|
`
|
|
963
927
|
);
|
|
@@ -978,7 +942,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
978
942
|
});
|
|
979
943
|
server.get("/filtered-tests.js", (_req, res) => {
|
|
980
944
|
const bytes = cachedContent.filteredTestCode?.length ?? null;
|
|
981
|
-
process.stdout.write(
|
|
945
|
+
config.debug && process.stdout.write(
|
|
982
946
|
`# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
|
|
983
947
|
`
|
|
984
948
|
);
|
|
@@ -997,100 +961,74 @@ function setupWebServer(config, cachedContent) {
|
|
|
997
961
|
});
|
|
998
962
|
res.end(cachedContent.filteredTestCode);
|
|
999
963
|
});
|
|
1000
|
-
server.get("/",
|
|
964
|
+
server.get("/", (_req, res) => {
|
|
1001
965
|
if (cachedContent._buildError) {
|
|
1002
|
-
const
|
|
1003
|
-
res.writeHead(200,
|
|
1004
|
-
res.
|
|
1005
|
-
|
|
1006
|
-
return
|
|
1007
|
-
`${config.projectRoot}/${config.output}/index.html`,
|
|
1008
|
-
htmlContent2
|
|
1009
|
-
);
|
|
966
|
+
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
967
|
+
res.writeHead(200, HTML_HEADERS);
|
|
968
|
+
res.end(htmlContent);
|
|
969
|
+
saveHTML(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
|
|
970
|
+
return;
|
|
1010
971
|
}
|
|
1011
972
|
if (cachedContent._noTestsWarning) {
|
|
1012
|
-
|
|
1013
|
-
res.
|
|
1014
|
-
res.write(htmlContent2);
|
|
1015
|
-
res.end();
|
|
973
|
+
res.writeHead(200, HTML_HEADERS);
|
|
974
|
+
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
1016
975
|
return;
|
|
1017
976
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
"./tests.js"
|
|
1022
|
-
);
|
|
1023
|
-
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1024
|
-
res.write(htmlContent);
|
|
1025
|
-
res.end();
|
|
1026
|
-
return await fsPromise.writeFile(
|
|
1027
|
-
`${config.projectRoot}/${config.output}/index.html`,
|
|
1028
|
-
htmlContent
|
|
1029
|
-
);
|
|
977
|
+
res.writeHead(200, HTML_HEADERS);
|
|
978
|
+
res.end(mainIndexHTML);
|
|
979
|
+
saveHTML(`${config.projectRoot}/${config.output}/index.html`, mainIndexHTML);
|
|
1030
980
|
});
|
|
1031
|
-
server.get("/qunitx.html",
|
|
981
|
+
server.get("/qunitx.html", (_req, res) => {
|
|
1032
982
|
if (cachedContent._buildError) {
|
|
1033
|
-
const
|
|
1034
|
-
res.writeHead(200,
|
|
1035
|
-
res.
|
|
1036
|
-
|
|
1037
|
-
return
|
|
1038
|
-
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1039
|
-
htmlContent2
|
|
1040
|
-
);
|
|
983
|
+
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
984
|
+
res.writeHead(200, HTML_HEADERS);
|
|
985
|
+
res.end(htmlContent);
|
|
986
|
+
saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
|
|
987
|
+
return;
|
|
1041
988
|
}
|
|
1042
989
|
if (cachedContent._noTestsWarning) {
|
|
1043
|
-
|
|
1044
|
-
res.
|
|
1045
|
-
res.write(htmlContent2);
|
|
1046
|
-
res.end();
|
|
990
|
+
res.writeHead(200, HTML_HEADERS);
|
|
991
|
+
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
1047
992
|
return;
|
|
1048
993
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
"./filtered-tests.js"
|
|
1053
|
-
);
|
|
1054
|
-
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1055
|
-
res.write(htmlContent);
|
|
1056
|
-
res.end();
|
|
1057
|
-
return await fsPromise.writeFile(
|
|
1058
|
-
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1059
|
-
htmlContent
|
|
1060
|
-
);
|
|
994
|
+
res.writeHead(200, HTML_HEADERS);
|
|
995
|
+
res.end(mainQunitxHTML);
|
|
996
|
+
saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, mainQunitxHTML);
|
|
1061
997
|
});
|
|
1062
|
-
server.get("/*",
|
|
998
|
+
server.get("/*", (req, res) => {
|
|
1063
999
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
1064
1000
|
if (possibleDynamicHTML) {
|
|
1065
1001
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
1066
1002
|
possibleDynamicHTML,
|
|
1067
|
-
|
|
1003
|
+
runtimeScript,
|
|
1068
1004
|
"/tests.js"
|
|
1069
1005
|
);
|
|
1070
|
-
res.writeHead(200,
|
|
1071
|
-
res.
|
|
1072
|
-
|
|
1073
|
-
return
|
|
1074
|
-
`${config.projectRoot}/${config.output}${req.path}`,
|
|
1075
|
-
htmlContent
|
|
1076
|
-
);
|
|
1006
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1007
|
+
res.end(htmlContent);
|
|
1008
|
+
saveHTML(`${config.projectRoot}/${config.output}${req.path}`, htmlContent);
|
|
1009
|
+
return;
|
|
1077
1010
|
}
|
|
1078
1011
|
const url = req.url;
|
|
1079
|
-
const requestStartedAt =
|
|
1012
|
+
const requestStartedAt = Date.now();
|
|
1080
1013
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1014
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1015
|
+
const stream = fs8.createReadStream(filePath);
|
|
1016
|
+
stream.on("open", () => {
|
|
1017
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1018
|
+
stream.pipe(res);
|
|
1019
|
+
config.debug && process.stdout.write(
|
|
1020
|
+
`# [HTTPServer] GET ${url} 200 - ${Date.now() - requestStartedAt}ms
|
|
1021
|
+
`
|
|
1022
|
+
);
|
|
1084
1023
|
});
|
|
1085
|
-
|
|
1086
|
-
res.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
process.stdout.write(
|
|
1091
|
-
`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms
|
|
1024
|
+
stream.on("error", () => {
|
|
1025
|
+
res.writeHead(404, { "Content-Type": contentType });
|
|
1026
|
+
res.end(contentType === MIME_TYPES.html ? NOT_FOUND_HTML : void 0);
|
|
1027
|
+
config.debug && process.stdout.write(
|
|
1028
|
+
`# [HTTPServer] GET ${url} 404 - ${Date.now() - requestStartedAt}ms
|
|
1092
1029
|
`
|
|
1093
|
-
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1094
1032
|
});
|
|
1095
1033
|
return server;
|
|
1096
1034
|
}
|
|
@@ -1102,7 +1040,8 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1102
1040
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1103
1041
|
}, html);
|
|
1104
1042
|
}
|
|
1105
|
-
function testRuntimeToInject(
|
|
1043
|
+
function testRuntimeToInject(config, groupId) {
|
|
1044
|
+
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1106
1045
|
return `<script>
|
|
1107
1046
|
window.testTimeout = 0;
|
|
1108
1047
|
setInterval(() => {
|
|
@@ -1138,7 +1077,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1138
1077
|
|
|
1139
1078
|
function setupWebSocket() {
|
|
1140
1079
|
try {
|
|
1141
|
-
window.socket = new WebSocket(
|
|
1080
|
+
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1142
1081
|
} catch (error) {
|
|
1143
1082
|
console.log(error);
|
|
1144
1083
|
retryOrFail();
|
|
@@ -1150,8 +1089,8 @@ function testRuntimeToInject(port, config) {
|
|
|
1150
1089
|
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1151
1090
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1152
1091
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1153
|
-
if (
|
|
1154
|
-
window.socket.send(JSON.stringify({ event: 'wsOpen' }));
|
|
1092
|
+
if (navigator.webdriver) {
|
|
1093
|
+
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1155
1094
|
}
|
|
1156
1095
|
maybeStart();
|
|
1157
1096
|
});
|
|
@@ -1159,9 +1098,9 @@ function testRuntimeToInject(port, config) {
|
|
|
1159
1098
|
retryOrFail();
|
|
1160
1099
|
});
|
|
1161
1100
|
window.socket.addEventListener('message', function(messageEvent) {
|
|
1162
|
-
if (!
|
|
1101
|
+
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1163
1102
|
window.location.reload(true);
|
|
1164
|
-
} else if (
|
|
1103
|
+
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1165
1104
|
window.abortQUnit = true;
|
|
1166
1105
|
window.QUnit.config.queue.length = 0;
|
|
1167
1106
|
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
@@ -1204,7 +1143,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1204
1143
|
|
|
1205
1144
|
if (!window.QUnit) {
|
|
1206
1145
|
console.log('QUnit not found after WebSocket connected');
|
|
1207
|
-
if (
|
|
1146
|
+
if (navigator.webdriver) {
|
|
1208
1147
|
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1209
1148
|
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1210
1149
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
@@ -1217,7 +1156,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1217
1156
|
}
|
|
1218
1157
|
|
|
1219
1158
|
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1220
|
-
if (
|
|
1159
|
+
if (navigator.webdriver) {
|
|
1221
1160
|
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1222
1161
|
}
|
|
1223
1162
|
});
|
|
@@ -1230,8 +1169,10 @@ function testRuntimeToInject(port, config) {
|
|
|
1230
1169
|
window.QUNIT_RESULT.finishedTests++;
|
|
1231
1170
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1232
1171
|
window.QUNIT_RESULT.currentTest = null;
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
1172
|
+
if (navigator.webdriver) {
|
|
1173
|
+
const isFailed = details.status === 'failed';
|
|
1174
|
+
const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
|
|
1175
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
|
|
1235
1176
|
|
|
1236
1177
|
if (${config.failFast} && details.status === 'failed') {
|
|
1237
1178
|
window.QUnit.config.queue.length = 0;
|
|
@@ -1239,7 +1180,7 @@ function testRuntimeToInject(port, config) {
|
|
|
1239
1180
|
}
|
|
1240
1181
|
});
|
|
1241
1182
|
window.QUnit.done((details) => {
|
|
1242
|
-
if (
|
|
1183
|
+
if (navigator.webdriver) {
|
|
1243
1184
|
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1244
1185
|
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1245
1186
|
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
@@ -1330,7 +1271,7 @@ function buildNoTestsHTML(files) {
|
|
|
1330
1271
|
</head>
|
|
1331
1272
|
<body>
|
|
1332
1273
|
<div id="qunit">
|
|
1333
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1274
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1334
1275
|
<h2 id="qunit-banner"></h2>
|
|
1335
1276
|
<div id="qunit-userAgent">Warning: No Tests Registered</div>
|
|
1336
1277
|
<ol id="qunit-tests">
|
|
@@ -1350,7 +1291,7 @@ function buildNoTestsHTML(files) {
|
|
|
1350
1291
|
(function () {
|
|
1351
1292
|
var retries = 0;
|
|
1352
1293
|
function connect() {
|
|
1353
|
-
var ws = new WebSocket(
|
|
1294
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1354
1295
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1355
1296
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1356
1297
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1432,7 +1373,7 @@ function buildErrorHTML(buildError) {
|
|
|
1432
1373
|
</head>
|
|
1433
1374
|
<body>
|
|
1434
1375
|
<div id="qunit">
|
|
1435
|
-
<h1 id="qunit-header">qunitx</h1>
|
|
1376
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1436
1377
|
<h2 id="qunit-banner"></h2>
|
|
1437
1378
|
<div id="qunit-userAgent">Build Error: ${buildError.type}</div>
|
|
1438
1379
|
<ol id="qunit-tests">
|
|
@@ -1452,7 +1393,7 @@ function buildErrorHTML(buildError) {
|
|
|
1452
1393
|
(function () {
|
|
1453
1394
|
var retries = 0;
|
|
1454
1395
|
function connect() {
|
|
1455
|
-
var ws = new WebSocket(
|
|
1396
|
+
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1456
1397
|
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1457
1398
|
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1458
1399
|
ws.addEventListener('error', function () { ws.close(); });
|
|
@@ -1464,16 +1405,182 @@ function buildErrorHTML(buildError) {
|
|
|
1464
1405
|
</body>
|
|
1465
1406
|
</html>`;
|
|
1466
1407
|
}
|
|
1467
|
-
|
|
1408
|
+
function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
1409
|
+
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
1410
|
+
groupCachedContent.mainHTML.html,
|
|
1411
|
+
groupCachedContent.mainHTML.filePath,
|
|
1412
|
+
groupConfig.projectRoot
|
|
1413
|
+
);
|
|
1414
|
+
const runtimeScript = testRuntimeToInject(groupConfig, groupId);
|
|
1415
|
+
const mainGroupHTML = escapeAndInjectTestsToHTML(
|
|
1416
|
+
mainHTMLWithReplacedAssets,
|
|
1417
|
+
runtimeScript,
|
|
1418
|
+
"./tests.js"
|
|
1419
|
+
);
|
|
1420
|
+
const saveHTML = (filePath, html) => fsPromise.writeFile(filePath, html).catch(
|
|
1421
|
+
(err) => groupConfig.debug && process.stderr.write(`# [qunitx] writeFile ${filePath}: ${err.message}
|
|
1422
|
+
`)
|
|
1423
|
+
);
|
|
1424
|
+
server.get(`/group-${groupId}/`, (_req, res) => {
|
|
1425
|
+
if (groupCachedContent._buildError) {
|
|
1426
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1427
|
+
res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (groupCachedContent._noTestsWarning) {
|
|
1431
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1432
|
+
res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1436
|
+
res.end(mainGroupHTML);
|
|
1437
|
+
saveHTML(`${groupConfig.projectRoot}/${groupConfig.output}/index.html`, mainGroupHTML);
|
|
1438
|
+
});
|
|
1439
|
+
server.get(`/group-${groupId}/tests.js`, (_req, res) => {
|
|
1440
|
+
const bytes = groupCachedContent.allTestCode?.length ?? null;
|
|
1441
|
+
if (bytes === null) {
|
|
1442
|
+
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
1443
|
+
res.end(
|
|
1444
|
+
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
1445
|
+
);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
groupConfig._onTestsJsServed?.();
|
|
1449
|
+
res.writeHead(200, {
|
|
1450
|
+
"Content-Type": "application/javascript",
|
|
1451
|
+
"Cache-Control": "no-store",
|
|
1452
|
+
"Content-Length": bytes
|
|
1453
|
+
});
|
|
1454
|
+
res.end(groupCachedContent.allTestCode);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
function debugGroupHeader(config) {
|
|
1458
|
+
const files = Object.keys(config.fsTree);
|
|
1459
|
+
const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
|
|
1460
|
+
const shown = rel.slice(0, 2);
|
|
1461
|
+
const rest = rel.length - shown.length;
|
|
1462
|
+
process.stdout.write(
|
|
1463
|
+
`# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
|
|
1464
|
+
`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
function setupGroupWSHandler(server, groupConfigs) {
|
|
1468
|
+
const socketToGroupId = /* @__PURE__ */ new WeakMap();
|
|
1469
|
+
server.wss.on("connection", function connection(socket) {
|
|
1470
|
+
socket.on("message", function message(data) {
|
|
1471
|
+
const { event, groupId, details, qunitResult, abort } = JSON.parse(data);
|
|
1472
|
+
let resolvedGroupId = socketToGroupId.get(socket);
|
|
1473
|
+
if (event === "wsOpen" && typeof groupId === "number") {
|
|
1474
|
+
resolvedGroupId = groupId;
|
|
1475
|
+
socketToGroupId.set(socket, groupId);
|
|
1476
|
+
}
|
|
1477
|
+
if (resolvedGroupId === void 0) return;
|
|
1478
|
+
const config = groupConfigs[resolvedGroupId];
|
|
1479
|
+
if (!config) return;
|
|
1480
|
+
if (event === "wsOpen") {
|
|
1481
|
+
config._phase = "loading";
|
|
1482
|
+
config._onWsOpen?.();
|
|
1483
|
+
} else if (event === "connection") {
|
|
1484
|
+
config._phase = "running";
|
|
1485
|
+
if (config.debug) debugGroupHeader(config);
|
|
1486
|
+
config._resetTestTimeout?.();
|
|
1487
|
+
} else if (event === "testEnd" && !abort) {
|
|
1488
|
+
if (details.status === "failed") {
|
|
1489
|
+
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1490
|
+
}
|
|
1491
|
+
if (config.debug && details.runtime > config.timeout * 0.8) {
|
|
1492
|
+
process.stdout.write(
|
|
1493
|
+
`# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}
|
|
1494
|
+
`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
config._resetTestTimeout?.();
|
|
1498
|
+
TAPDisplayTestResult(config.COUNTER, details);
|
|
1499
|
+
} else if (event === "done") {
|
|
1500
|
+
config._phase = "done";
|
|
1501
|
+
config._lastQUnitResult = qunitResult ?? null;
|
|
1502
|
+
if (config.debug) {
|
|
1503
|
+
process.stdout.write(
|
|
1504
|
+
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
1505
|
+
`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
if (typeof config._testRunDone === "function") {
|
|
1509
|
+
config._testRunDone();
|
|
1510
|
+
config._testRunDone = null;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
function registerSharedStaticHandler(server, groupConfigs) {
|
|
1517
|
+
const groupUrlRegex = /^\/group-(\d+)(\/.*)?$/;
|
|
1518
|
+
server.get("/*", (req, res) => {
|
|
1519
|
+
const match = groupUrlRegex.exec(req.path);
|
|
1520
|
+
if (!match) {
|
|
1521
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1522
|
+
res.end("Not found");
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const groupId = parseInt(match[1], 10);
|
|
1526
|
+
const groupConfig = groupConfigs[groupId];
|
|
1527
|
+
if (!groupConfig) {
|
|
1528
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1529
|
+
res.end("Not found");
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const STATIC_FILES_PATH = path4.join(groupConfig.projectRoot, groupConfig.output);
|
|
1533
|
+
const subPath = match[2] || "/";
|
|
1534
|
+
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
1535
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1536
|
+
const stream = fs8.createReadStream(filePath);
|
|
1537
|
+
stream.on("open", () => {
|
|
1538
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1539
|
+
stream.pipe(res);
|
|
1540
|
+
});
|
|
1541
|
+
stream.on("error", () => {
|
|
1542
|
+
res.writeHead(404, { "Content-Type": contentType });
|
|
1543
|
+
res.end(contentType === MIME_TYPES.html ? NOT_FOUND_HTML : void 0);
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
var fsPromise, HTML_HEADERS, NOT_FOUND_HTML;
|
|
1468
1548
|
var init_web_server = __esm({
|
|
1469
1549
|
"lib/setup/web-server.ts"() {
|
|
1470
1550
|
init_find_internal_assets_from_html();
|
|
1471
1551
|
init_html();
|
|
1472
1552
|
init_display_test_result();
|
|
1473
1553
|
init_color();
|
|
1474
|
-
init_path_exists();
|
|
1475
1554
|
init_http();
|
|
1476
1555
|
fsPromise = fs8.promises;
|
|
1556
|
+
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1557
|
+
NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
1558
|
+
<html lang="en">
|
|
1559
|
+
<head>
|
|
1560
|
+
<meta charset="utf-8">
|
|
1561
|
+
<meta name="viewport" content="width=device-width">
|
|
1562
|
+
<title>404 Not Found \u2014 qunitx</title>
|
|
1563
|
+
<style>
|
|
1564
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1565
|
+
body{font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial,sans-serif}
|
|
1566
|
+
#qunit-header{padding:.5em 0 .5em 1em;color:#C2CCD1;background-color:#0D3349;font-size:1.5em;line-height:1em;font-weight:400;border-radius:5px 5px 0 0}
|
|
1567
|
+
#qunit-banner{height:5px;background-color:#EE5757}
|
|
1568
|
+
#qunit-userAgent{padding:.5em 1em;color:#fff;background-color:#2B81AF;text-shadow:rgba(0,0,0,.5) 2px 2px 1px;font-size:small}
|
|
1569
|
+
#qunit-tests{list-style:none;font-size:smaller}
|
|
1570
|
+
#qunit-tests li{display:list-item;padding:.4em 1em;color:#000;background-color:#EE5757;border-radius:0 0 5px 5px}
|
|
1571
|
+
</style>
|
|
1572
|
+
</head>
|
|
1573
|
+
<body>
|
|
1574
|
+
<div id="qunit">
|
|
1575
|
+
<h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
|
|
1576
|
+
<h2 id="qunit-banner"></h2>
|
|
1577
|
+
<div id="qunit-userAgent">404 Not Found</div>
|
|
1578
|
+
<ol id="qunit-tests">
|
|
1579
|
+
<li id="qunit-testresult"><script>document.getElementById('qunit-testresult').prepend(location.pathname)</script> was not found on this server.</li>
|
|
1580
|
+
</ol>
|
|
1581
|
+
</div>
|
|
1582
|
+
</body>
|
|
1583
|
+
</html>`;
|
|
1477
1584
|
}
|
|
1478
1585
|
});
|
|
1479
1586
|
|
|
@@ -1522,22 +1629,43 @@ async function launchBrowser(config) {
|
|
|
1522
1629
|
handleSIGHUP: false
|
|
1523
1630
|
});
|
|
1524
1631
|
}
|
|
1525
|
-
async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
1632
|
+
async function setupBrowser(config, cachedContent, existingBrowser = null, sharedServer = null) {
|
|
1526
1633
|
const setupStart = Date.now();
|
|
1527
|
-
const [server,
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1634
|
+
const [server, browser, page] = await (async () => {
|
|
1635
|
+
if (sharedServer) {
|
|
1636
|
+
const newPage2 = await existingBrowser.newPage();
|
|
1637
|
+
perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
|
|
1638
|
+
return [sharedServer, existingBrowser, newPage2];
|
|
1639
|
+
}
|
|
1640
|
+
const [newServer, resolvedBrowser] = await Promise.all([
|
|
1641
|
+
setupWebServer(config, cachedContent),
|
|
1642
|
+
Promise.resolve(existingBrowser)
|
|
1643
|
+
]);
|
|
1644
|
+
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1645
|
+
const activeBrowser = resolvedBrowser ?? await launchBrowser(config);
|
|
1646
|
+
const pageStart = Date.now();
|
|
1647
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1648
|
+
const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
1649
|
+
const [newPage] = await Promise.all([getPage(), bindServerToPort(newServer, config)]);
|
|
1650
|
+
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
1651
|
+
return [newServer, activeBrowser, newPage];
|
|
1652
|
+
})();
|
|
1653
|
+
if (config.browser === "firefox") {
|
|
1654
|
+
await page.addInitScript(() => {
|
|
1655
|
+
const preSerialize = (arg) => {
|
|
1656
|
+
if (arg === null || typeof arg !== "object") return arg;
|
|
1657
|
+
try {
|
|
1658
|
+
return JSON.stringify(arg, (_key, v) => v instanceof Date ? v.toISOString() : v);
|
|
1659
|
+
} catch {
|
|
1660
|
+
return String(arg);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
["log", "warn", "error", "info", "debug"].forEach((method) => {
|
|
1664
|
+
const orig = console[method].bind(console);
|
|
1665
|
+
console[method] = (...args) => orig(...args.map(preSerialize));
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1541
1669
|
config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
|
|
1542
1670
|
page.on("console", (msg) => {
|
|
1543
1671
|
const type = msg.type();
|
|
@@ -1545,11 +1673,7 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1545
1673
|
if (!alwaysShow && !config.debug) return;
|
|
1546
1674
|
const handler = (async () => {
|
|
1547
1675
|
try {
|
|
1548
|
-
const values = await Promise.all(
|
|
1549
|
-
msg.args().map(
|
|
1550
|
-
(arg) => arg.jsonValue().catch(() => arg.evaluate((v) => JSON.stringify(v)).then(JSON.parse))
|
|
1551
|
-
)
|
|
1552
|
-
);
|
|
1676
|
+
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
|
|
1553
1677
|
console.log(...values);
|
|
1554
1678
|
} catch {
|
|
1555
1679
|
console.log(msg.text());
|
|
@@ -1847,8 +1971,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1847
1971
|
fs9.writeFile(
|
|
1848
1972
|
`${projectRoot}/${output}/qunitx.html`,
|
|
1849
1973
|
buildErrorHTML(cachedContent._buildError)
|
|
1850
|
-
).catch(
|
|
1851
|
-
|
|
1974
|
+
).catch(
|
|
1975
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
1976
|
+
`)
|
|
1977
|
+
);
|
|
1852
1978
|
}
|
|
1853
1979
|
if (config.watch) {
|
|
1854
1980
|
console.log(`# ${exception}`);
|
|
@@ -1880,9 +2006,6 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1880
2006
|
);
|
|
1881
2007
|
}
|
|
1882
2008
|
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
1883
|
-
const RETRY_DELAY_MS = 100;
|
|
1884
|
-
const MAX_RETRIES = 3;
|
|
1885
|
-
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1886
2009
|
let { result, js } = await getContents();
|
|
1887
2010
|
const initialSize = js.length;
|
|
1888
2011
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
@@ -2032,7 +2155,110 @@ async function flushConsoleHandlers(handlers, deadline = Date.now() + 2e3) {
|
|
|
2032
2155
|
await Promise.allSettled([...handlers]);
|
|
2033
2156
|
return flushConsoleHandlers(handlers, deadline);
|
|
2034
2157
|
}
|
|
2035
|
-
|
|
2158
|
+
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2159
|
+
groupCachedContents.forEach((cachedContent) => {
|
|
2160
|
+
cachedContent._buildError = null;
|
|
2161
|
+
cachedContent._noTestsWarning = null;
|
|
2162
|
+
});
|
|
2163
|
+
const { projectRoot, debug, watch, browser } = groupConfigs[0];
|
|
2164
|
+
const activeGroups = groupConfigs.reduce(
|
|
2165
|
+
(acc, groupConfig, groupIndex) => {
|
|
2166
|
+
const files = Object.keys(groupConfig.fsTree);
|
|
2167
|
+
if (files.length > 0)
|
|
2168
|
+
acc.push({
|
|
2169
|
+
groupIndex,
|
|
2170
|
+
config: groupConfig,
|
|
2171
|
+
cachedContent: groupCachedContents[groupIndex],
|
|
2172
|
+
files
|
|
2173
|
+
});
|
|
2174
|
+
return acc;
|
|
2175
|
+
},
|
|
2176
|
+
[]
|
|
2177
|
+
);
|
|
2178
|
+
if (activeGroups.length === 0)
|
|
2179
|
+
return console.log(
|
|
2180
|
+
"# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
|
|
2181
|
+
);
|
|
2182
|
+
await Promise.all(
|
|
2183
|
+
activeGroups.map(
|
|
2184
|
+
(group) => fs9.mkdir(`${group.config.projectRoot}/${group.config.output}`, { recursive: true })
|
|
2185
|
+
)
|
|
2186
|
+
);
|
|
2187
|
+
const sourcemap = debug ? "inline" : watch ? "linked" : false;
|
|
2188
|
+
const groupEntryPlugin = {
|
|
2189
|
+
name: "group-entry-loader",
|
|
2190
|
+
setup(build) {
|
|
2191
|
+
build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
|
|
2192
|
+
path: args.path,
|
|
2193
|
+
namespace: "group-entry"
|
|
2194
|
+
}));
|
|
2195
|
+
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2196
|
+
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2197
|
+
return {
|
|
2198
|
+
contents: activeGroups[slotIndex].files.map((filePath) => `import "${filePath}";`).join(""),
|
|
2199
|
+
resolveDir: process.cwd()
|
|
2200
|
+
};
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
const buildOptions = {
|
|
2205
|
+
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2206
|
+
in: `group-entry-${slotIndex}`,
|
|
2207
|
+
out: `group-${slotIndex}`
|
|
2208
|
+
})),
|
|
2209
|
+
plugins: [groupEntryPlugin],
|
|
2210
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2211
|
+
bundle: true,
|
|
2212
|
+
logLevel: "silent",
|
|
2213
|
+
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2214
|
+
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2215
|
+
outdir: path5.join(projectRoot, "tmp"),
|
|
2216
|
+
keepNames: true,
|
|
2217
|
+
legalComments: "none",
|
|
2218
|
+
target: esbuildTarget(browser),
|
|
2219
|
+
sourcemap,
|
|
2220
|
+
write: false,
|
|
2221
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2222
|
+
};
|
|
2223
|
+
const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
|
|
2224
|
+
(outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
|
|
2225
|
+
);
|
|
2226
|
+
const buildWithRetry = async (retriesLeft) => {
|
|
2227
|
+
const result = await esbuild.build(buildOptions);
|
|
2228
|
+
if (!hasSmallOutput(result) || retriesLeft === 0) return result;
|
|
2229
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2230
|
+
return buildWithRetry(retriesLeft - 1);
|
|
2231
|
+
};
|
|
2232
|
+
try {
|
|
2233
|
+
const result = await buildWithRetry(MAX_RETRIES);
|
|
2234
|
+
await Promise.all(
|
|
2235
|
+
(result.outputFiles ?? []).map((outputFile) => {
|
|
2236
|
+
const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
|
|
2237
|
+
if (!match) return Promise.resolve();
|
|
2238
|
+
const slotIndex = parseInt(match[1]);
|
|
2239
|
+
const isMap = Boolean(match[2]);
|
|
2240
|
+
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2241
|
+
const destPath = `${config.projectRoot}/${config.output}/tests.js${isMap ? ".map" : ""}`;
|
|
2242
|
+
if (!isMap) cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2243
|
+
return fs9.writeFile(destPath, outputFile.contents);
|
|
2244
|
+
})
|
|
2245
|
+
);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
2248
|
+
const errorHtml = buildErrorHTML(buildError);
|
|
2249
|
+
await Promise.all(
|
|
2250
|
+
activeGroups.map((group) => {
|
|
2251
|
+
group.cachedContent._buildError = buildError;
|
|
2252
|
+
return fs9.writeFile(`${group.config.projectRoot}/${group.config.output}/index.html`, errorHtml).catch(
|
|
2253
|
+
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2254
|
+
`)
|
|
2255
|
+
);
|
|
2256
|
+
})
|
|
2257
|
+
);
|
|
2258
|
+
throw error;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, GROUP_OUTPUT_REGEX;
|
|
2036
2262
|
var init_tests_in_browser = __esm({
|
|
2037
2263
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2038
2264
|
init_color();
|
|
@@ -2052,6 +2278,10 @@ var init_tests_in_browser = __esm({
|
|
|
2052
2278
|
(_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
|
|
2053
2279
|
);
|
|
2054
2280
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
2281
|
+
RETRY_DELAY_MS = 100;
|
|
2282
|
+
MAX_RETRIES = 3;
|
|
2283
|
+
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2284
|
+
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2055
2285
|
}
|
|
2056
2286
|
});
|
|
2057
2287
|
|
|
@@ -2346,7 +2576,9 @@ var init_write_output_static_files = __esm({
|
|
|
2346
2576
|
// lib/commands/run.ts
|
|
2347
2577
|
var run_exports = {};
|
|
2348
2578
|
__export(run_exports, {
|
|
2579
|
+
computeFileTimes: () => computeFileTimes,
|
|
2349
2580
|
default: () => run,
|
|
2581
|
+
readTimingCache: () => readTimingCache,
|
|
2350
2582
|
run: () => run
|
|
2351
2583
|
});
|
|
2352
2584
|
import fs12 from "node:fs/promises";
|
|
@@ -2422,7 +2654,8 @@ async function run(config) {
|
|
|
2422
2654
|
} else {
|
|
2423
2655
|
const allFiles = Object.keys(config.fsTree);
|
|
2424
2656
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2425
|
-
const
|
|
2657
|
+
const timings = await readTimingCache(config.projectRoot);
|
|
2658
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2426
2659
|
config.COUNTER = {
|
|
2427
2660
|
testCount: 0,
|
|
2428
2661
|
failCount: 0,
|
|
@@ -2441,6 +2674,13 @@ async function run(config) {
|
|
|
2441
2674
|
_phase: "bundling"
|
|
2442
2675
|
}));
|
|
2443
2676
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
2677
|
+
const sharedServer = groupCount > 1 && cachedContent.htmlPathsToRunTests[0] === "/" && cachedContent.htmlPathsToRunTests.length === 1 ? (() => {
|
|
2678
|
+
const s = new HTTPServer();
|
|
2679
|
+
setupGroupWSHandler(s, groupConfigs);
|
|
2680
|
+
groupConfigs.forEach((gc, i) => registerGroupRoutes(s, gc, groupCachedContents[i], i));
|
|
2681
|
+
registerSharedStaticHandler(s, groupConfigs);
|
|
2682
|
+
return s;
|
|
2683
|
+
})() : null;
|
|
2444
2684
|
process.stdout.write("TAP version 13\n");
|
|
2445
2685
|
process.stdout.write(
|
|
2446
2686
|
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
|
|
@@ -2448,22 +2688,27 @@ async function run(config) {
|
|
|
2448
2688
|
);
|
|
2449
2689
|
const [browser] = await Promise.all([
|
|
2450
2690
|
launchBrowser(config),
|
|
2451
|
-
|
|
2452
|
-
groupConfigs.
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2691
|
+
sharedServer ? bindServerToPort(sharedServer, config).then(
|
|
2692
|
+
() => groupConfigs.forEach((gc, i) => {
|
|
2693
|
+
gc.port = config.port;
|
|
2694
|
+
groupCachedContents[i].htmlPathsToRunTests = [`/group-${i}/`];
|
|
2695
|
+
})
|
|
2696
|
+
) : Promise.resolve(),
|
|
2697
|
+
Promise.all([
|
|
2698
|
+
groupCount > 1 ? buildAllGroupBundles(groupConfigs, groupCachedContents) : buildTestBundle(groupConfigs[0], groupCachedContents[0]),
|
|
2699
|
+
Promise.all(
|
|
2700
|
+
groupConfigs.map((gc, i) => writeOutputStaticFiles(gc, groupCachedContents[i]))
|
|
2457
2701
|
)
|
|
2458
|
-
)
|
|
2702
|
+
])
|
|
2459
2703
|
]);
|
|
2460
2704
|
if (config.open) {
|
|
2461
2705
|
void openOutputInBrowser(config);
|
|
2462
2706
|
}
|
|
2463
2707
|
const TIME_COUNTER = timeCounter();
|
|
2708
|
+
const wallTimes = /* @__PURE__ */ new Map();
|
|
2464
2709
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2465
2710
|
const keepAlive = setInterval(() => {
|
|
2466
|
-
},
|
|
2711
|
+
}, 1e4);
|
|
2467
2712
|
const groupResults = await Promise.allSettled(
|
|
2468
2713
|
groupConfigs.map((groupConfig, i) => {
|
|
2469
2714
|
const groupTimeout = new Promise((_, reject) => {
|
|
@@ -2480,35 +2725,41 @@ async function run(config) {
|
|
|
2480
2725
|
}, GROUP_TIMEOUT_MS);
|
|
2481
2726
|
timeoutId.unref();
|
|
2482
2727
|
});
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
groupConfig
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2728
|
+
const startMs = Date.now();
|
|
2729
|
+
const work = (async () => {
|
|
2730
|
+
groupConfig._phase = "connecting";
|
|
2731
|
+
const connections = await setupBrowser(
|
|
2732
|
+
groupConfig,
|
|
2733
|
+
groupCachedContents[i],
|
|
2734
|
+
browser,
|
|
2735
|
+
sharedServer
|
|
2736
|
+
);
|
|
2737
|
+
groupConfig.expressApp = connections.server;
|
|
2738
|
+
if (config.before) {
|
|
2739
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
2740
|
+
}
|
|
2741
|
+
try {
|
|
2742
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
2743
|
+
} finally {
|
|
2744
|
+
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
2745
|
+
await Promise.all([
|
|
2746
|
+
!sharedServer && connections.server?.close(),
|
|
2747
|
+
connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
2748
|
+
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
2749
|
+
Promise.race([
|
|
2750
|
+
connections.page.close(),
|
|
2751
|
+
new Promise((resolve) => {
|
|
2752
|
+
const pageCloseTimeoutId = setTimeout(resolve, 1e4);
|
|
2753
|
+
pageCloseTimeoutId.unref();
|
|
2506
2754
|
})
|
|
2507
|
-
])
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2755
|
+
]).catch(() => {
|
|
2756
|
+
})
|
|
2757
|
+
]);
|
|
2758
|
+
}
|
|
2759
|
+
})();
|
|
2760
|
+
const record = () => wallTimes.set(i, Date.now() - startMs);
|
|
2761
|
+
work.then(record, record);
|
|
2762
|
+
return Promise.race([work, groupTimeout]);
|
|
2512
2763
|
})
|
|
2513
2764
|
);
|
|
2514
2765
|
const exitCode = groupResults.reduce(
|
|
@@ -2527,6 +2778,12 @@ async function run(config) {
|
|
|
2527
2778
|
);
|
|
2528
2779
|
}
|
|
2529
2780
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
2781
|
+
const fileTimes = computeFileTimes(groups, weights, wallTimes);
|
|
2782
|
+
persistTimings(fileTimes, config.projectRoot).catch(
|
|
2783
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] persistTimings: ${err.message}
|
|
2784
|
+
`)
|
|
2785
|
+
);
|
|
2786
|
+
printFileTimings(fileTimes, config.projectRoot);
|
|
2530
2787
|
if (config.after) {
|
|
2531
2788
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2532
2789
|
}
|
|
@@ -2535,8 +2792,16 @@ async function run(config) {
|
|
|
2535
2792
|
process.stdout.write("\n", async () => {
|
|
2536
2793
|
clearTimeout(exitTimer);
|
|
2537
2794
|
clearInterval(keepAlive);
|
|
2538
|
-
await
|
|
2539
|
-
|
|
2795
|
+
await Promise.all([
|
|
2796
|
+
sharedServer?.close().catch(
|
|
2797
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
2798
|
+
`)
|
|
2799
|
+
),
|
|
2800
|
+
browser.close().catch(
|
|
2801
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] browser.close: ${err.message}
|
|
2802
|
+
`)
|
|
2803
|
+
)
|
|
2804
|
+
]);
|
|
2540
2805
|
await shutdownPrelaunch();
|
|
2541
2806
|
process.exit(exitCode);
|
|
2542
2807
|
});
|
|
@@ -2597,24 +2862,57 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
2597
2862
|
}
|
|
2598
2863
|
return cachedContent;
|
|
2599
2864
|
}
|
|
2600
|
-
async function
|
|
2601
|
-
|
|
2865
|
+
async function readTimingCache(projectRoot) {
|
|
2866
|
+
try {
|
|
2867
|
+
const parsed = JSON.parse(await fs12.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
2868
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
2869
|
+
} catch {
|
|
2870
|
+
return {};
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function computeFileTimes(groups, weights, wallTimes) {
|
|
2874
|
+
const result = /* @__PURE__ */ new Map();
|
|
2875
|
+
groups.forEach((group, i) => {
|
|
2876
|
+
const wallMs = wallTimes.get(i);
|
|
2877
|
+
if (wallMs === void 0) return;
|
|
2878
|
+
const total = group.reduce((sum, f) => sum + (weights.get(f) ?? 0), 0);
|
|
2879
|
+
group.forEach(
|
|
2880
|
+
(f) => result.set(f, total > 0 ? wallMs * ((weights.get(f) ?? 0) / total) : wallMs / group.length)
|
|
2881
|
+
);
|
|
2882
|
+
});
|
|
2883
|
+
return result;
|
|
2884
|
+
}
|
|
2885
|
+
async function persistTimings(fileTimes, projectRoot) {
|
|
2886
|
+
await fs12.writeFile(
|
|
2887
|
+
`${projectRoot}/tmp/test-timings.json`,
|
|
2888
|
+
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
function printFileTimings(fileTimes, projectRoot) {
|
|
2892
|
+
if (fileTimes.size === 0) return;
|
|
2893
|
+
const lines = [...fileTimes.entries()].sort(([, a], [, b]) => b - a).map(([f, ms]) => `# ${ms.toFixed(0)}ms ${f.replace(`${projectRoot}/`, "")}`);
|
|
2894
|
+
process.stdout.write(`# File execution times:
|
|
2895
|
+
${lines.join("\n")}
|
|
2896
|
+
`);
|
|
2897
|
+
}
|
|
2898
|
+
async function splitIntoGroups(files, groupCount, timings) {
|
|
2899
|
+
const sizes = await Promise.all(
|
|
2602
2900
|
files.map(
|
|
2603
|
-
(f) => fs12.stat(f).then((
|
|
2901
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs12.stat(f).then((s) => s.size).catch(() => 0)
|
|
2604
2902
|
)
|
|
2605
2903
|
);
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2904
|
+
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
2905
|
+
const msPerByte = knownRates.length > 0 ? knownRates.reduce((sum, { ms, size }) => sum + ms / size, 0) / knownRates.length : 1;
|
|
2906
|
+
const weights = new Map(
|
|
2907
|
+
files.map((f, i) => [f, timings[f] > 0 ? timings[f] : sizes[i] * msPerByte])
|
|
2908
|
+
);
|
|
2909
|
+
const buckets = Array.from({ length: groupCount }, () => ({ files: [], total: 0 }));
|
|
2910
|
+
[...files].sort((a, b) => (weights.get(b) ?? 0) - (weights.get(a) ?? 0)).forEach((f) => {
|
|
2911
|
+
const min = buckets.reduce((m, _, i) => buckets[i].total < buckets[m].total ? i : m, 0);
|
|
2912
|
+
buckets[min].files.push(f);
|
|
2913
|
+
buckets[min].total += weights.get(f) ?? 0;
|
|
2914
|
+
});
|
|
2915
|
+
return { groups: buckets.filter((b) => b.files.length > 0).map((b) => b.files), weights };
|
|
2618
2916
|
}
|
|
2619
2917
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
2620
2918
|
const prefix = "Watching files...";
|
|
@@ -2637,6 +2935,9 @@ var init_run = __esm({
|
|
|
2637
2935
|
"lib/commands/run.ts"() {
|
|
2638
2936
|
init_browser();
|
|
2639
2937
|
init_chrome_prelaunch();
|
|
2938
|
+
init_http();
|
|
2939
|
+
init_bind_server_to_port();
|
|
2940
|
+
init_web_server();
|
|
2640
2941
|
init_open_output_in_browser();
|
|
2641
2942
|
init_color();
|
|
2642
2943
|
init_tests_in_browser();
|
|
@@ -2663,7 +2964,7 @@ init_color();
|
|
|
2663
2964
|
var package_default = {
|
|
2664
2965
|
name: "qunitx-cli",
|
|
2665
2966
|
type: "module",
|
|
2666
|
-
version: "0.19.
|
|
2967
|
+
version: "0.19.3",
|
|
2667
2968
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2668
2969
|
author: "Izel Nakri",
|
|
2669
2970
|
license: "MIT",
|
|
@@ -2779,8 +3080,18 @@ import path2 from "node:path";
|
|
|
2779
3080
|
// lib/utils/find-project-root.ts
|
|
2780
3081
|
import process2 from "node:process";
|
|
2781
3082
|
|
|
3083
|
+
// lib/utils/path-exists.ts
|
|
3084
|
+
import fs2 from "node:fs/promises";
|
|
3085
|
+
async function pathExists(path7) {
|
|
3086
|
+
try {
|
|
3087
|
+
await fs2.access(path7);
|
|
3088
|
+
return true;
|
|
3089
|
+
} catch {
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2782
3094
|
// lib/utils/search-in-parent-directories.ts
|
|
2783
|
-
init_path_exists();
|
|
2784
3095
|
async function searchInParentDirectories(directory, targetEntry) {
|
|
2785
3096
|
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
2786
3097
|
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
@@ -2808,9 +3119,6 @@ async function findProjectRoot() {
|
|
|
2808
3119
|
}
|
|
2809
3120
|
}
|
|
2810
3121
|
|
|
2811
|
-
// lib/commands/init.ts
|
|
2812
|
-
init_path_exists();
|
|
2813
|
-
|
|
2814
3122
|
// lib/setup/default-project-config-values.ts
|
|
2815
3123
|
var defaultProjectConfigValues = {
|
|
2816
3124
|
output: "tmp",
|
|
@@ -2877,7 +3185,6 @@ async function writeTSConfigIfNeeded(projectRoot) {
|
|
|
2877
3185
|
// lib/commands/generate.ts
|
|
2878
3186
|
init_color();
|
|
2879
3187
|
import fs5 from "node:fs/promises";
|
|
2880
|
-
init_path_exists();
|
|
2881
3188
|
init_read_template();
|
|
2882
3189
|
|
|
2883
3190
|
// lib/utils/convert-to-pascal-case.ts
|
|
@@ -3162,6 +3469,7 @@ process4.title = "qunitx";
|
|
|
3162
3469
|
} catch (error) {
|
|
3163
3470
|
console.error(error);
|
|
3164
3471
|
process4.exitCode = 1;
|
|
3472
|
+
await shutdownPrelaunch();
|
|
3165
3473
|
process4.stdout.write("\n", () => process4.exit(1));
|
|
3166
3474
|
}
|
|
3167
3475
|
})();
|