browser-ava 1.0.0
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/LICENSE +23 -0
- package/README.md +32 -0
- package/package.json +60 -0
- package/src/browser/ava.mjs +87 -0
- package/src/browser/eql.mjs +73 -0
- package/src/browser/favicon.ico +0 -0
- package/src/browser/index.css +40 -0
- package/src/browser/index.html +15 -0
- package/src/browser/runtime.mjs +427 -0
- package/src/browser/util.mjs +72 -0
- package/src/browser-ava-cli.mjs +203 -0
- package/src/resolver.mjs +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2021-2022 by arlac77
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
18
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
19
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
20
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
23
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/browser-ava)
|
|
2
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
3
|
+
[](https://bundlejs.com/?q=browser-ava)
|
|
4
|
+
[](https://npmjs.org/package/browser-ava)
|
|
5
|
+
[](https://github.com/arlac77/browser-ava/issues)
|
|
6
|
+
[](https://actions-badge.atrox.dev/arlac77/browser-ava/goto)
|
|
7
|
+
[](https://coveralls.io/github/arlac77/browser-ava)
|
|
8
|
+
# browser-ava
|
|
9
|
+
Run ava tests in the browser
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
If your code does not depend on any node api (process, fs, ...) then this runner allows to run your ava test inside the browser.
|
|
15
|
+
|
|
16
|
+
### Running your tests
|
|
17
|
+
|
|
18
|
+
```console
|
|
19
|
+
browser-ava --webkit --chromium --firefox tests/*.mjs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## limitations
|
|
24
|
+
|
|
25
|
+
- only supports ESM
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## install
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
npm -g install browser-ava
|
|
32
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browser-ava",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Run ava tests in the browser",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ava",
|
|
10
|
+
"runner",
|
|
11
|
+
"test",
|
|
12
|
+
"testing"
|
|
13
|
+
],
|
|
14
|
+
"contributors": [
|
|
15
|
+
{
|
|
16
|
+
"name": "Markus Felten",
|
|
17
|
+
"email": "markus.felten@gmx.de"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"license": "BSD-2-Clause",
|
|
21
|
+
"bin": {
|
|
22
|
+
"browser-ava": "src/browser-ava-cli.mjs"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "npm run test:ava",
|
|
26
|
+
"test:ava": "ava --timeout 2m tests/*.mjs",
|
|
27
|
+
"cover": "c8 -x 'tests/**/*' --temp-directory build/tmp ava --timeout 2m tests/*.mjs && c8 report -r lcov -o build/coverage --temp-directory build/tmp"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^9.4.1",
|
|
31
|
+
"es-module-lexer": "^1.0.5",
|
|
32
|
+
"koa": "^2.13.4",
|
|
33
|
+
"koa-static": "^5.0.0",
|
|
34
|
+
"playwright": "^1.27.1",
|
|
35
|
+
"ws": "^8.9.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"ava": "^5.0.1",
|
|
39
|
+
"c8": "^7.12.0",
|
|
40
|
+
"execa": "^6.1.0",
|
|
41
|
+
"semantic-release": "^19.0.5"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=16.18.0"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/arlac77/browser-ava.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/arlac77/browser-ava/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/arlac77/browser-ava#readme",
|
|
54
|
+
"template": {
|
|
55
|
+
"inheritFrom": [
|
|
56
|
+
"arlac77/template-arlac77-github",
|
|
57
|
+
"arlac77/template-node-app"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds all tests
|
|
3
|
+
*/
|
|
4
|
+
export const testModules = [];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collect all tests into testModules
|
|
8
|
+
*/
|
|
9
|
+
export default function test(body, ...args) {
|
|
10
|
+
let title;
|
|
11
|
+
if (typeof body === "string") {
|
|
12
|
+
title = body;
|
|
13
|
+
body = args.shift();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (body.title) {
|
|
17
|
+
title = body.title(title, ...args);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const def = { title, body, args };
|
|
21
|
+
testModules.at(-1).tests.push(def);
|
|
22
|
+
return def;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test.failing = (...args) => {
|
|
26
|
+
test(...args).failing = true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
test.skip = (...args) => {
|
|
30
|
+
test(...args).skip = true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
test.only = (...args) => {
|
|
34
|
+
test(...args).only = true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
test.serial = (...args) => {
|
|
38
|
+
test(...args).serial = true;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
test.todo = title => {
|
|
42
|
+
const def = { title, todo: true };
|
|
43
|
+
testModules.at(-1).tests.push(def);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
test.serial.todo = test.todo;
|
|
47
|
+
|
|
48
|
+
test.before = (...args) => {
|
|
49
|
+
const def = { args };
|
|
50
|
+
testModules.at(-1).before.push(def);
|
|
51
|
+
return def;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
test.serial.before = (...args) => {
|
|
55
|
+
test.before(...args).serial = true;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
test.after = (...args) => {
|
|
59
|
+
const def = { args };
|
|
60
|
+
testModules.at(-1).after.push(def);
|
|
61
|
+
return def;
|
|
62
|
+
};
|
|
63
|
+
test.after.always = (...args) => {
|
|
64
|
+
test.after(...args).always = true;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
test.serial.after = (...args) => {
|
|
68
|
+
test.after(...args).serial = true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
test.beforeEach = (...args) => {
|
|
72
|
+
const def = { args };
|
|
73
|
+
testModules.at(-1).beforeEach.push(def);
|
|
74
|
+
return def;
|
|
75
|
+
};
|
|
76
|
+
test.beforeEach.always = (...args) => {
|
|
77
|
+
test.beforeEach(...args).always = true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
test.afterEach = (...args) => {
|
|
81
|
+
const def = { args };
|
|
82
|
+
testModules.at(-1).afterEach.push(def);
|
|
83
|
+
return def;
|
|
84
|
+
};
|
|
85
|
+
test.afterEach.always = () => {
|
|
86
|
+
test.afterEach(...args).always = true;
|
|
87
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function isEqual(a, b) {
|
|
2
|
+
if (a !== undefined && b === undefined) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (isScalar(a)) {
|
|
7
|
+
return Object.is(a, b);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (Array.isArray(a)) {
|
|
11
|
+
if (a.length === b.length) {
|
|
12
|
+
for (let i = 0; i < a.length; i++) {
|
|
13
|
+
if (!isEqual(a[i], b[i])) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof a === "object") {
|
|
24
|
+
if (a instanceof Set) {
|
|
25
|
+
return (
|
|
26
|
+
b instanceof Set &&
|
|
27
|
+
a.size === b.size &&
|
|
28
|
+
[...a].every((value) => b.has(value))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (a instanceof Map) {
|
|
32
|
+
if (!(b instanceof Map) || a.size !== b.size) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (const [k, v] of a.entries()) {
|
|
36
|
+
if (!isEqual(v, b.get(k))) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const key of new Set(Object.keys(a).concat(Object.keys(b)))) {
|
|
45
|
+
if (!isEqual(a[key], b[key])) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scalarTypes = new Set([
|
|
57
|
+
"symbol",
|
|
58
|
+
"undefined",
|
|
59
|
+
"string",
|
|
60
|
+
"number",
|
|
61
|
+
"bigint",
|
|
62
|
+
"boolean",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export function isScalar(a) {
|
|
66
|
+
return (
|
|
67
|
+
scalarTypes.has(typeof a) ||
|
|
68
|
+
a instanceof String ||
|
|
69
|
+
a instanceof Number ||
|
|
70
|
+
a instanceof Function ||
|
|
71
|
+
a === null
|
|
72
|
+
);
|
|
73
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
.running {
|
|
3
|
+
font-weight: bold;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.passed {
|
|
7
|
+
color: #006100;
|
|
8
|
+
background-color: #c6efce;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.failed {
|
|
12
|
+
color: #9c0006;
|
|
13
|
+
background-color: #ffc7ce;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.todo, .skip {
|
|
17
|
+
color: #9c6500;
|
|
18
|
+
background-color: #ffeb9c;
|
|
19
|
+
}
|
|
20
|
+
#summary {
|
|
21
|
+
font-weight: bold;
|
|
22
|
+
padding: 20px 50px;
|
|
23
|
+
}
|
|
24
|
+
.hidePassed ~ ul>li.passed,
|
|
25
|
+
.hidePassed>ul>li.passed {display: none;}
|
|
26
|
+
.module{
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
.module::marker {
|
|
30
|
+
content: "▼ ";//⮟
|
|
31
|
+
}
|
|
32
|
+
.module.hidePassed::marker {
|
|
33
|
+
content: "▶ ";//➤
|
|
34
|
+
}
|
|
35
|
+
li:has(>.module)::marker {
|
|
36
|
+
content: "▼ ";//⮟
|
|
37
|
+
}
|
|
38
|
+
li:has(>.module.hidePassed)::marker {
|
|
39
|
+
content: "▶ ";//➤
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf8" />
|
|
5
|
+
<meta name="application-name" content="AVA test runner"/>
|
|
6
|
+
<title>AVA test runner</title>
|
|
7
|
+
<link rel="stylesheet" href="index.css" />
|
|
8
|
+
<script type="module" src="runtime.mjs"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<button id="run">run</button>
|
|
12
|
+
<div id="tests"></div>
|
|
13
|
+
<div id="summary"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { testModules } from "./ava.mjs";
|
|
2
|
+
import { calculateSummary, summaryMessages, pluralize, stringify } from "./util.mjs";
|
|
3
|
+
import { isEqual } from "./eql.mjs";
|
|
4
|
+
|
|
5
|
+
let ws = new WebSocket(`ws://${location.host}`);
|
|
6
|
+
ws.onerror = console.error;
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
forward console info,log,error to the server
|
|
10
|
+
*/
|
|
11
|
+
for (const slot of ["log", "info", "error"]) {
|
|
12
|
+
const former = console[slot];
|
|
13
|
+
|
|
14
|
+
console[slot] = (...args) => {
|
|
15
|
+
if (ws) {
|
|
16
|
+
ws.send(stringify({ action: slot, data: args }));
|
|
17
|
+
}
|
|
18
|
+
former(...args);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ws.onmessage = async message => {
|
|
23
|
+
const data = JSON.parse(message.data);
|
|
24
|
+
switch (data.action) {
|
|
25
|
+
case "load":
|
|
26
|
+
{
|
|
27
|
+
testModules.length = 0;
|
|
28
|
+
let errors = 0;
|
|
29
|
+
for (const tm of data.data) {
|
|
30
|
+
tm.logs = [];
|
|
31
|
+
tm.tests = [];
|
|
32
|
+
tm.before = [];
|
|
33
|
+
tm.after = [];
|
|
34
|
+
tm.beforeEach = [];
|
|
35
|
+
tm.afterEach = [];
|
|
36
|
+
testModules.push(tm);
|
|
37
|
+
try {
|
|
38
|
+
await import(new URL(tm.url, import.meta.url));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
errors++;
|
|
41
|
+
console.error(e.toString());
|
|
42
|
+
tm.logs.push(`error importing ${tm.url} ${e}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
displayTests();
|
|
47
|
+
if (errors === 0) {
|
|
48
|
+
ws.send(stringify({ action: "ready" }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "run": {
|
|
53
|
+
await runTestModules();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async function displayTests() {
|
|
59
|
+
function renderTest(t) {
|
|
60
|
+
return `<li class="test ${
|
|
61
|
+
t.passed === true
|
|
62
|
+
? "passed"
|
|
63
|
+
: t.passed === false
|
|
64
|
+
? "failed"
|
|
65
|
+
: t.skip
|
|
66
|
+
? "skip"
|
|
67
|
+
: t.todo
|
|
68
|
+
? "todo"
|
|
69
|
+
: ""
|
|
70
|
+
}">${t.title} <span>${
|
|
71
|
+
t.assertions
|
|
72
|
+
? t.assertions
|
|
73
|
+
.filter(a => !a.passed)
|
|
74
|
+
.map(a => (a.title || "") + " " + (a.message || ""))
|
|
75
|
+
.join(" ")
|
|
76
|
+
: ""
|
|
77
|
+
}</span>${t.message ? t.message : ""}</li>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderModule(tm) {
|
|
81
|
+
const passedTestsCount = tm.tests.filter(t => t.passed).length;
|
|
82
|
+
const allTestsCount = tm.tests.length;
|
|
83
|
+
return `<li id="${tm.file}" class="module${
|
|
84
|
+
passedTestsCount === allTestsCount ? " passed" : ""
|
|
85
|
+
}">
|
|
86
|
+
<span class="moduleName">${tm.file}</span>
|
|
87
|
+
<span class="moduleSummary"> ( ${passedTestsCount} / ${allTestsCount} ${pluralize(
|
|
88
|
+
"test",
|
|
89
|
+
allTestsCount
|
|
90
|
+
)} passed )</span>
|
|
91
|
+
<div class="logs">${tm.logs.join("<br/>")}</div>
|
|
92
|
+
<ul>${tm.tests.map(renderTest).join("\n")}</ul>
|
|
93
|
+
</li>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tests = document.getElementById("tests");
|
|
97
|
+
tests.innerHTML = `<ul class="wrapper"><li>
|
|
98
|
+
<span class="all module">ALL TESTS</span>
|
|
99
|
+
<ul class="allTests">${testModules.map(renderModule).join("\n")}</ul>
|
|
100
|
+
</li></ul>`;
|
|
101
|
+
|
|
102
|
+
tests.querySelectorAll(".module").forEach(elem => {
|
|
103
|
+
elem.onclick = switchPassed;
|
|
104
|
+
manualSwitchPassed(elem);
|
|
105
|
+
});
|
|
106
|
+
function switchPassed() {
|
|
107
|
+
manualSwitchPassed(this);
|
|
108
|
+
console.log(this);
|
|
109
|
+
}
|
|
110
|
+
function manualSwitchPassed(elem) {
|
|
111
|
+
elem.classList.toggle("hidePassed");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
document.getElementById("summary").innerHTML = summaryMessages(
|
|
115
|
+
calculateSummary(testModules)
|
|
116
|
+
)
|
|
117
|
+
.map(m => m.html)
|
|
118
|
+
.join("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function execHooks(hooks, t) {
|
|
122
|
+
if (hooks.length > 0) {
|
|
123
|
+
await Promise.all(
|
|
124
|
+
hooks.map(async h => {
|
|
125
|
+
h.args[typeof h.args[0] === "string" ? 1 : 0](t);
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runTest(parent, tm, test) {
|
|
132
|
+
if (!test.skip && !test.todo) {
|
|
133
|
+
const t = testContext(test, parent);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await execHooks(tm.beforeEach, t);
|
|
137
|
+
|
|
138
|
+
if (t.ms) {
|
|
139
|
+
t.timer = setTimeout(() => {
|
|
140
|
+
t.passed = false;
|
|
141
|
+
t.log("Test timeout exceeded");
|
|
142
|
+
}, t.ms);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await test.body(t, ...test.args);
|
|
146
|
+
|
|
147
|
+
if (t.timer) {
|
|
148
|
+
clearTimeout(t.timer);
|
|
149
|
+
delete t.timer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const td of t.teardowns.reverse()) {
|
|
153
|
+
await td();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await execHooks(tm.afterEach, t);
|
|
157
|
+
|
|
158
|
+
if (test.assertions.length === 0) {
|
|
159
|
+
test.passed = false;
|
|
160
|
+
test.message = "Test finished without running any assertions";
|
|
161
|
+
} else {
|
|
162
|
+
test.passed = !test.assertions.find(
|
|
163
|
+
a => a.passed !== true && !a.skipped
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (t.planned !== undefined && t.planned !== test.assertions.length) {
|
|
167
|
+
test.passed = false;
|
|
168
|
+
test.message = `Planned for ${t.planned} assertions, but got ${test.assertions.length}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
test.passed = false;
|
|
173
|
+
test.message = e;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const runButton = document.getElementById("run");
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* run serial tests before all others
|
|
182
|
+
*/
|
|
183
|
+
async function runTestModule(tm) {
|
|
184
|
+
runButton.classList.add("running");
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
tm.logs = [];
|
|
188
|
+
|
|
189
|
+
const t = {
|
|
190
|
+
context: {},
|
|
191
|
+
log(...args) {
|
|
192
|
+
tm.logs.push(args);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await execHooks(tm.before, t);
|
|
197
|
+
|
|
198
|
+
for (const test of tm.tests.filter(test => test.serial)) {
|
|
199
|
+
await runTest(t, tm, test);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await Promise.all(
|
|
203
|
+
tm.tests.filter(test => !test.serial).map(test => runTest(t, tm, test))
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await execHooks(tm.after, t);
|
|
207
|
+
} finally {
|
|
208
|
+
runButton.classList.remove("running");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runTestModules() {
|
|
213
|
+
await Promise.all(testModules.map(tm => runTestModule(tm)));
|
|
214
|
+
|
|
215
|
+
ws.send(stringify({ action: "result", data: testModules }));
|
|
216
|
+
|
|
217
|
+
displayTests();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
runButton.onclick = runTestModules;
|
|
221
|
+
|
|
222
|
+
function testContext(def, parentContext) {
|
|
223
|
+
def.assertions = [];
|
|
224
|
+
def.logs = [];
|
|
225
|
+
|
|
226
|
+
function throwsExpectationHandler(e, expectation, title) {
|
|
227
|
+
if (expectation !== undefined) {
|
|
228
|
+
for (const slot of ["name", "code", "is"]) {
|
|
229
|
+
if (expectation[slot] !== undefined) {
|
|
230
|
+
if (expectation[slot] !== e[slot]) {
|
|
231
|
+
def.assertions.push({
|
|
232
|
+
passed: false,
|
|
233
|
+
message: `expected ${slot}=${expectation[slot]} but got ${e[slot]}`,
|
|
234
|
+
title
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (expectation.message !== undefined) {
|
|
241
|
+
const slot = "message";
|
|
242
|
+
if (expectation.message instanceof RegExp) {
|
|
243
|
+
if (!expectation.message.test(e.message)) {
|
|
244
|
+
def.assertions.push({
|
|
245
|
+
passed: false,
|
|
246
|
+
message: `${slot} does not match ${expectation[slot]}`,
|
|
247
|
+
title
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof expectation.message === "string") {
|
|
254
|
+
if (expectation[slot] !== e[slot]) {
|
|
255
|
+
def.assertions.push({
|
|
256
|
+
passed: false,
|
|
257
|
+
message: `expected ${slot}=${expectation[slot]} but got ${e[slot]}`,
|
|
258
|
+
title
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
def.assertions.push({ passed: true, title });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const assertions = {
|
|
270
|
+
pass(title) {
|
|
271
|
+
def.assertions.push({ passed: true, title });
|
|
272
|
+
},
|
|
273
|
+
fail(title) {
|
|
274
|
+
def.assertions.push({
|
|
275
|
+
passed: false,
|
|
276
|
+
title,
|
|
277
|
+
message: "Test failed via `t.fail()`"
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
throws(a, expectation, title) {
|
|
282
|
+
try {
|
|
283
|
+
a();
|
|
284
|
+
def.assertions.push({
|
|
285
|
+
passed: false,
|
|
286
|
+
title,
|
|
287
|
+
message: "Expected exception to be thrown"
|
|
288
|
+
});
|
|
289
|
+
} catch (e) {
|
|
290
|
+
throwsExpectationHandler(e, expectation, title);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async throwsAsync(a, expectation, title) {
|
|
295
|
+
try {
|
|
296
|
+
await a();
|
|
297
|
+
def.assertions.push({
|
|
298
|
+
passed: false,
|
|
299
|
+
title,
|
|
300
|
+
message: "Expected exception to be thrown"
|
|
301
|
+
});
|
|
302
|
+
} catch (e) {
|
|
303
|
+
throwsExpectationHandler(e, expectation, title);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
notThrows(a, title) {
|
|
308
|
+
try {
|
|
309
|
+
a();
|
|
310
|
+
} catch (e) {
|
|
311
|
+
def.assertions.push({
|
|
312
|
+
passed: false,
|
|
313
|
+
title,
|
|
314
|
+
message: `Unexpected exception ${e}`
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
async notThrowsAsync(a, title) {
|
|
320
|
+
try {
|
|
321
|
+
await a();
|
|
322
|
+
} catch (e) {
|
|
323
|
+
def.assertions.push({
|
|
324
|
+
passed: false,
|
|
325
|
+
title,
|
|
326
|
+
message: `Unexpected exception ${e}`
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
deepEqual(a, b, title) {
|
|
332
|
+
def.assertions.push({
|
|
333
|
+
passed: isEqual(a, b),
|
|
334
|
+
message: `${a} != ${b}`,
|
|
335
|
+
title
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
notDeepEqual(a, b, title) {
|
|
339
|
+
def.assertions.push({
|
|
340
|
+
passed: !isEqual(a, b),
|
|
341
|
+
message: `${a} = ${b}`,
|
|
342
|
+
title
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
regex(contents, regex, message) {
|
|
347
|
+
def.assertions.push({
|
|
348
|
+
passed: contents.match(regex) ? true : false,
|
|
349
|
+
message: `${contents} matches ${regex}`
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
notRegex(contents, regex, message) {
|
|
353
|
+
def.assertions.push({
|
|
354
|
+
passed: contents.match(regex) ? false : true,
|
|
355
|
+
message: `${contents} matches ${regex}`
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
is(a, b, title) {
|
|
360
|
+
def.assertions.push({
|
|
361
|
+
passed: Object.is(a, b),
|
|
362
|
+
message: `${a} != ${b}`,
|
|
363
|
+
title
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
not(a, b, title) {
|
|
367
|
+
def.assertions.push({
|
|
368
|
+
passed: !Object.is(a, b),
|
|
369
|
+
message: `${a} = ${b}`,
|
|
370
|
+
title
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
true(value, title) {
|
|
375
|
+
def.assertions.push({
|
|
376
|
+
passed: value === true,
|
|
377
|
+
message: `${value} != true`,
|
|
378
|
+
title
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
truthy(value, title) {
|
|
382
|
+
def.assertions.push({
|
|
383
|
+
passed: value ? true : false,
|
|
384
|
+
message: `${value} is not truthy`,
|
|
385
|
+
title
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
false(value, title) {
|
|
389
|
+
def.assertions.push({
|
|
390
|
+
passed: value === false,
|
|
391
|
+
message: `${value} != false`,
|
|
392
|
+
title
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
falsy(value, title) {
|
|
396
|
+
def.assertions.push({
|
|
397
|
+
passed: value ? false : true,
|
|
398
|
+
message: `${value} is not falsy`,
|
|
399
|
+
title
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
Object.values(assertions).forEach(
|
|
405
|
+
assertion => (assertion.skip = () => def.assertions.push({ skipped: true }))
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
...assertions,
|
|
410
|
+
...parentContext,
|
|
411
|
+
teardowns: [],
|
|
412
|
+
title: def.title,
|
|
413
|
+
log(...args) {
|
|
414
|
+
def.logs.push(args);
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
plan(count) {
|
|
418
|
+
this.planned = count;
|
|
419
|
+
},
|
|
420
|
+
teardown(fn) {
|
|
421
|
+
this.teardowns.push(fn);
|
|
422
|
+
},
|
|
423
|
+
timeout(ms) {
|
|
424
|
+
this.ms = ms;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function calculateSummary(testModules) {
|
|
2
|
+
let failed = 0,
|
|
3
|
+
knownFailure = 0,
|
|
4
|
+
todo = 0,
|
|
5
|
+
skip = 0,
|
|
6
|
+
passed = 0;
|
|
7
|
+
|
|
8
|
+
for (const tm of testModules) {
|
|
9
|
+
for (const test of tm.tests) {
|
|
10
|
+
if (test.skip) {
|
|
11
|
+
skip++;
|
|
12
|
+
} else {
|
|
13
|
+
if (test.todo) {
|
|
14
|
+
todo++;
|
|
15
|
+
} else {
|
|
16
|
+
if (test.passed) {
|
|
17
|
+
passed++;
|
|
18
|
+
} else {
|
|
19
|
+
if (test.failing) {
|
|
20
|
+
knownFailure++;
|
|
21
|
+
} else {
|
|
22
|
+
if (test.passed === false) {
|
|
23
|
+
failed++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { passed, failed, knownFailure, skip, todo };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pluralize(word, number) {
|
|
36
|
+
return number > 1 ? word + "s" : word;
|
|
37
|
+
}
|
|
38
|
+
export function summaryMessages(summary) {
|
|
39
|
+
const messages = [];
|
|
40
|
+
|
|
41
|
+
function message(number, word, template, colorClass = "") {
|
|
42
|
+
if (number >= 1) {
|
|
43
|
+
const text = template
|
|
44
|
+
.replace(/{number}/, number)
|
|
45
|
+
.replace(/{word}/, pluralize(word, number));
|
|
46
|
+
messages.push({
|
|
47
|
+
colorClass,
|
|
48
|
+
text,
|
|
49
|
+
html: `<div class="${colorClass}">${text}</div>`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
message(summary.passed, "test", "{number} {word} passed", "passed");
|
|
55
|
+
message(summary.failed, "test", "{number} {word} failed", "failed");
|
|
56
|
+
message(summary.knownFailure, "failure", "{number} known {word}", "failed");
|
|
57
|
+
message(summary.skip, "test", "{number} {word} skipped", "skip");
|
|
58
|
+
message(summary.todo, "test", "{number} {word} todo", "todo");
|
|
59
|
+
|
|
60
|
+
return messages;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @TODO HACK to be able to sent BigInt
|
|
65
|
+
*/
|
|
66
|
+
export function stringify(...args) {
|
|
67
|
+
const former = BigInt.prototype.toJSON;
|
|
68
|
+
BigInt.prototype.toJSON = (v) => v.toString();
|
|
69
|
+
const string = JSON.stringify(...args);
|
|
70
|
+
BigInt.prototype.toJSON = former;
|
|
71
|
+
return string;
|
|
72
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { init, parse } from "es-module-lexer";
|
|
6
|
+
import { chromium, firefox, webkit } from "playwright";
|
|
7
|
+
import Koa from "koa";
|
|
8
|
+
import Static from "koa-static";
|
|
9
|
+
import { WebSocketServer } from "ws";
|
|
10
|
+
import { program, Option } from "commander";
|
|
11
|
+
import { calculateSummary, summaryMessages } from "./browser/util.mjs";
|
|
12
|
+
import { resolveImport } from "./resolver.mjs";
|
|
13
|
+
|
|
14
|
+
const utf8EncodingOptions = { encoding: "utf8" };
|
|
15
|
+
|
|
16
|
+
const { version, description } = JSON.parse(
|
|
17
|
+
readFileSync(
|
|
18
|
+
new URL("../package.json", import.meta.url).pathname,
|
|
19
|
+
utf8EncodingOptions
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const knownBrowsers = {
|
|
24
|
+
chromium: chromium,
|
|
25
|
+
firefox: firefox,
|
|
26
|
+
webkit: webkit,
|
|
27
|
+
safari: webkit
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const browsers = [];
|
|
31
|
+
let openBrowsers = [];
|
|
32
|
+
|
|
33
|
+
Object.entries(knownBrowsers).forEach(([name, browser]) => {
|
|
34
|
+
program.option(`--${name}`, `run tests against ${name} browser`, () =>
|
|
35
|
+
browsers.push(browser)
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.description(description)
|
|
41
|
+
.version(version)
|
|
42
|
+
.addOption(
|
|
43
|
+
new Option("-p, --port <number>", "server port to use")
|
|
44
|
+
.default(8080)
|
|
45
|
+
.env("PORT")
|
|
46
|
+
)
|
|
47
|
+
.addOption(
|
|
48
|
+
new Option("-b, --browser <name>", "browser to use").env("BROWSER")
|
|
49
|
+
)
|
|
50
|
+
.option("--headless", "hide browser window", false)
|
|
51
|
+
.option(
|
|
52
|
+
"--no-keep-open",
|
|
53
|
+
"keep browser-ava and the page open after execution",
|
|
54
|
+
true
|
|
55
|
+
)
|
|
56
|
+
.argument("<tests...>")
|
|
57
|
+
.action(async (tests, options) => {
|
|
58
|
+
if (options.browser) {
|
|
59
|
+
browsers.push(knownBrowsers[options.browser]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (browsers.length === 0) {
|
|
63
|
+
console.error(
|
|
64
|
+
"No browsers selected use --webkit, --chromium and/or --firefox"
|
|
65
|
+
);
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await init;
|
|
70
|
+
|
|
71
|
+
tests = tests.map(file => {
|
|
72
|
+
return { url: resolve(process.cwd(), file), file };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { server, wss } = await createServer(tests, options);
|
|
76
|
+
|
|
77
|
+
async function shutdown(failed, force) {
|
|
78
|
+
if (!options.keepOpen || force) {
|
|
79
|
+
await Promise.all(openBrowsers.map(browser => browser.close()));
|
|
80
|
+
server.close();
|
|
81
|
+
process.exit(force ? 2 : failed ? 1 : 0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let errors = 0;
|
|
86
|
+
|
|
87
|
+
wss.on("connection", ws => {
|
|
88
|
+
ws.on("message", async data => {
|
|
89
|
+
data = JSON.parse(data);
|
|
90
|
+
switch (data.action) {
|
|
91
|
+
case "info":
|
|
92
|
+
console.info(...data.data);
|
|
93
|
+
break;
|
|
94
|
+
case "log":
|
|
95
|
+
console.log(...data.data);
|
|
96
|
+
break;
|
|
97
|
+
case "error":
|
|
98
|
+
errors++;
|
|
99
|
+
console.error(...data.data);
|
|
100
|
+
await shutdown(undefined, true);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "ready":
|
|
104
|
+
ws.send(JSON.stringify({ action: "run" }));
|
|
105
|
+
break;
|
|
106
|
+
case "result":
|
|
107
|
+
const summary = calculateSummary(data.data);
|
|
108
|
+
|
|
109
|
+
for (const m of summaryMessages(summary)) {
|
|
110
|
+
console.log(m.text);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await shutdown(summary.failed);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
ws.send(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
action: "load",
|
|
120
|
+
data: tests
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
openBrowsers = await Promise.all(
|
|
126
|
+
browsers.map(async b => {
|
|
127
|
+
const browser = await b.launch({ headless: options.headless });
|
|
128
|
+
const page = await browser.newPage();
|
|
129
|
+
await page.goto(`http://localhost:${options.port}/index.html`);
|
|
130
|
+
return browser;
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
program.parse(process.argv);
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async function loadAndRewriteImports(file) {
|
|
139
|
+
let body = await readFile(file, utf8EncodingOptions);
|
|
140
|
+
|
|
141
|
+
const [imports] = parse(body);
|
|
142
|
+
|
|
143
|
+
let d = 0;
|
|
144
|
+
|
|
145
|
+
for (const i of imports) {
|
|
146
|
+
let m;
|
|
147
|
+
|
|
148
|
+
if (i.n === "ava") {
|
|
149
|
+
// m = new URL("browser/ava.mjs", import.meta.url).pathname;
|
|
150
|
+
m = "/ava.mjs";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!m) {
|
|
154
|
+
m = await resolveImport(i.n, resolve(process.cwd(), file));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (m) {
|
|
158
|
+
body = body.substring(0, i.s + d) + m + body.substring(i.e + d);
|
|
159
|
+
d += m.length - i.n.length;
|
|
160
|
+
} else {
|
|
161
|
+
console.warn(`Unable to resolve "${i.n}" may lead to import errors`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function createServer(tests, options) {
|
|
169
|
+
const app = new Koa();
|
|
170
|
+
|
|
171
|
+
app.use(Static(new URL("./browser", import.meta.url).pathname));
|
|
172
|
+
|
|
173
|
+
app.on("error", console.error);
|
|
174
|
+
|
|
175
|
+
app.use(async (ctx, next) => {
|
|
176
|
+
const path = ctx.request.path;
|
|
177
|
+
|
|
178
|
+
if (path.endsWith(".mjs") || path.endsWith(".js")) {
|
|
179
|
+
ctx.response.type = "text/javascript";
|
|
180
|
+
ctx.body = await loadAndRewriteImports(path);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await next();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const server = await new Promise((resolve, reject) => {
|
|
188
|
+
const server = app.listen(options.port, error => {
|
|
189
|
+
if (error) {
|
|
190
|
+
reject(error);
|
|
191
|
+
} else {
|
|
192
|
+
resolve(server);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const wss = new WebSocketServer({ server });
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
server,
|
|
201
|
+
wss
|
|
202
|
+
};
|
|
203
|
+
}
|
package/src/resolver.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { join, dirname, resolve } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const utf8EncodingOptions = { encoding: "utf8" };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Order in which imports are searched
|
|
8
|
+
* @see {https://nodejs.org/dist/latest/docs/api/packages.html#imports}
|
|
9
|
+
*/
|
|
10
|
+
const importsConditionOrder = ["browser", "default"];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Order in which exports are searched
|
|
14
|
+
* @see {https://nodejs.org/dist/latest/docs/api/packages.html#exports}
|
|
15
|
+
*/
|
|
16
|
+
const exportsConditionOrder = ["browser", "import", ".", "default"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* find module inside a package
|
|
20
|
+
* @param {string} parts
|
|
21
|
+
* @param {Object} pkg package.json content
|
|
22
|
+
* @returns {string|undefined} module file name relative to package
|
|
23
|
+
*/
|
|
24
|
+
export function resolveExports(parts, pkg) {
|
|
25
|
+
function matchingCondition(value) {
|
|
26
|
+
switch (typeof value) {
|
|
27
|
+
case "string":
|
|
28
|
+
return value;
|
|
29
|
+
case "object":
|
|
30
|
+
for (const condition of exportsConditionOrder) {
|
|
31
|
+
if (value[condition]) {
|
|
32
|
+
return value[condition];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (parts[0] === pkg.name) {
|
|
39
|
+
switch (parts.length) {
|
|
40
|
+
case 1:
|
|
41
|
+
return matchingCondition(pkg.exports) || pkg.main || "index.js";
|
|
42
|
+
default:
|
|
43
|
+
return matchingCondition(pkg.exports["./" + parts.slice(1).join("/")]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveImports(name, pkg) {
|
|
49
|
+
if (name.match(/^#/)) {
|
|
50
|
+
const importSlot = pkg.imports[name];
|
|
51
|
+
if (importSlot) {
|
|
52
|
+
for (const condition of importsConditionOrder) {
|
|
53
|
+
if (importSlot[condition]) {
|
|
54
|
+
return importSlot[condition];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadPackage(path) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(
|
|
64
|
+
await readFile(join(path, "package.json"), utf8EncodingOptions)
|
|
65
|
+
);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (e.code !== "ENOTDIR" && e.code !== "ENOENT") {
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function findPackage(path) {
|
|
74
|
+
while (path.length) {
|
|
75
|
+
const pkg = await loadPackage(path);
|
|
76
|
+
if (pkg) {
|
|
77
|
+
return {
|
|
78
|
+
path,
|
|
79
|
+
pkg
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
path = dirname(path);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Maps import url from node to browser view.
|
|
88
|
+
* @param {string} name module to resolve
|
|
89
|
+
* @param {string} file where to start resolving (base)
|
|
90
|
+
* @returns {Promise<string>} resolved import url
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveImport(name, file) {
|
|
93
|
+
if (name.match(/^[\/\.]/)) {
|
|
94
|
+
return resolve(dirname(file), name);
|
|
95
|
+
}
|
|
96
|
+
let { pkg, path } = await findPackage(file);
|
|
97
|
+
|
|
98
|
+
const parts = name.split(/\//);
|
|
99
|
+
|
|
100
|
+
const e = resolveExports(parts, pkg) || resolveImports(name, pkg);
|
|
101
|
+
|
|
102
|
+
if (e) {
|
|
103
|
+
return join(path, e);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
while (path.length > 1) {
|
|
107
|
+
const p = join(path, "node_modules", parts[0]);
|
|
108
|
+
pkg = await loadPackage(p);
|
|
109
|
+
if (pkg) {
|
|
110
|
+
return join(p, resolveExports(parts, pkg));
|
|
111
|
+
}
|
|
112
|
+
path = dirname(dirname(path));
|
|
113
|
+
}
|
|
114
|
+
}
|