@w-lfpup/jackrabbit 0.1.0 → 0.3.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.
Files changed (98) hide show
  1. package/.github/workflows/browsers.json +45 -0
  2. package/.github/workflows/browsers.macos.json +51 -0
  3. package/.github/workflows/browsers.windows.json +19 -0
  4. package/.github/workflows/tests.yml +42 -0
  5. package/README.md +151 -8
  6. package/browser/dist/logger.js +43 -0
  7. package/browser/dist/mod.js +26 -0
  8. package/browser/dist/queue.js +27 -0
  9. package/browser/dist/runner.js +20 -0
  10. package/{cli → browser}/package.json +1 -1
  11. package/browser/src/logger.ts +57 -0
  12. package/browser/src/mod.ts +30 -0
  13. package/browser/src/runner.ts +22 -0
  14. package/browser/tsconfig.json +11 -0
  15. package/browser/tsconfig.tsbuildinfo +1 -0
  16. package/browsers.json +38 -0
  17. package/core/dist/jackrabbit_types.d.ts +62 -28
  18. package/core/dist/mod.d.ts +2 -2
  19. package/core/dist/mod.js +1 -1
  20. package/core/dist/run_steps.d.ts +2 -2
  21. package/core/dist/run_steps.js +83 -67
  22. package/core/src/jackrabbit_types.ts +73 -29
  23. package/core/src/mod.ts +2 -8
  24. package/core/src/run_steps.ts +111 -80
  25. package/examples/hello_world/goodbye_world.ts +1 -1
  26. package/examples/hello_world/hello_world.ts +1 -1
  27. package/nodejs/dist/logger.js +161 -0
  28. package/nodejs/dist/mod.js +31 -0
  29. package/nodejs/dist/results.js +139 -0
  30. package/nodejs/dist/results_str.js +147 -0
  31. package/nodejs/dist/runner.js +17 -0
  32. package/nodejs/src/logger.ts +193 -0
  33. package/nodejs/src/mod.ts +37 -0
  34. package/nodejs/src/results_str.ts +234 -0
  35. package/{cli → nodejs}/tsconfig.json +2 -1
  36. package/nodejs/tsconfig.tsbuildinfo +1 -0
  37. package/package.json +9 -6
  38. package/tests/dist/mod.d.ts +14 -3
  39. package/tests/dist/mod.js +33 -13
  40. package/tests/dist/test_error.test.d.ts +9 -0
  41. package/tests/dist/test_error.test.js +27 -0
  42. package/tests/dist/test_errors.test.d.ts +9 -0
  43. package/tests/dist/test_errors.test.js +27 -0
  44. package/tests/dist/test_logger.d.ts +3 -2
  45. package/tests/dist/test_logger.js +5 -1
  46. package/tests/src/mod.ts +31 -15
  47. package/tests/src/test_error.test.ts +32 -0
  48. package/tests/src/test_logger.ts +6 -1
  49. package/tests/tsconfig.tsbuildinfo +1 -1
  50. package/tsconfig.json +1 -1
  51. package/webdriver/dist/config.js +57 -0
  52. package/webdriver/dist/eventbus.js +18 -0
  53. package/webdriver/dist/listeners.js +21 -0
  54. package/webdriver/dist/logger.js +203 -0
  55. package/webdriver/dist/mod.js +36 -0
  56. package/webdriver/dist/results_str.js +167 -0
  57. package/webdriver/dist/routes.js +172 -0
  58. package/webdriver/dist/routes2.js +163 -0
  59. package/webdriver/dist/test_hangar.js +20 -0
  60. package/webdriver/dist/webdriver.js +273 -0
  61. package/webdriver/package.json +8 -0
  62. package/webdriver/src/config.ts +89 -0
  63. package/webdriver/src/eventbus.ts +104 -0
  64. package/webdriver/src/logger.ts +247 -0
  65. package/webdriver/src/mod.ts +43 -0
  66. package/webdriver/src/results.ts +56 -0
  67. package/webdriver/src/results_str.ts +222 -0
  68. package/webdriver/src/routes.ts +211 -0
  69. package/webdriver/src/test_hangar.ts +25 -0
  70. package/webdriver/src/webdriver.ts +372 -0
  71. package/{nodejs_cli → webdriver}/tsconfig.json +1 -0
  72. package/webdriver/tsconfig.tsbuildinfo +1 -0
  73. package/.github/workflows/build_and_test.yml +0 -18
  74. package/cli/dist/cli.d.ts +0 -3
  75. package/cli/dist/cli.js +0 -8
  76. package/cli/dist/cli_types.d.ts +0 -7
  77. package/cli/dist/config.d.ts +0 -5
  78. package/cli/dist/config.js +0 -27
  79. package/cli/dist/importer.d.ts +0 -7
  80. package/cli/dist/importer.js +0 -16
  81. package/cli/dist/logger.d.ts +0 -7
  82. package/cli/dist/logger.js +0 -88
  83. package/cli/dist/mod.d.ts +0 -6
  84. package/cli/dist/mod.js +0 -4
  85. package/cli/src/cli.ts +0 -17
  86. package/cli/src/cli_types.ts +0 -9
  87. package/cli/src/config.ts +0 -36
  88. package/cli/src/importer.ts +0 -25
  89. package/cli/src/logger.ts +0 -126
  90. package/cli/src/mod.ts +0 -7
  91. package/cli/tsconfig.tsbuildinfo +0 -1
  92. package/nodejs_cli/dist/mod.d.ts +0 -2
  93. package/nodejs_cli/dist/mod.js +0 -20
  94. package/nodejs_cli/src/mod.ts +0 -25
  95. package/nodejs_cli/tsconfig.tsbuildinfo +0 -1
  96. package/test_guide.md +0 -114
  97. /package/{nodejs_cli → nodejs}/package.json +0 -0
  98. /package/{cli/dist/cli_types.js → webdriver/dist/results.js} +0 -0
@@ -0,0 +1,167 @@
1
+ const SPACE = " ";
2
+ export function getResultsAsString(sessionResults) {
3
+ const output = [];
4
+ // Lots of nested loops because results a nested structure.
5
+ // I'd rather see composition nested in one function
6
+ // than have for loops spread across each function.
7
+ logSessionErrors(output, sessionResults);
8
+ for (let [, result] of sessionResults.runs) {
9
+ if (logRunResults(output, result))
10
+ continue;
11
+ for (const collection of result.collections) {
12
+ if (logCollectionResult(output, collection))
13
+ continue;
14
+ if (collection)
15
+ for (const moduleResult of collection.modules) {
16
+ if (logModuleResult(output, moduleResult))
17
+ continue;
18
+ if (moduleResult)
19
+ for (const testResult of moduleResult.testResults) {
20
+ logTest(output, testResult);
21
+ }
22
+ }
23
+ }
24
+ }
25
+ logSummary(output, sessionResults);
26
+ return output.join("\n");
27
+ }
28
+ function logSessionErrors(output, sessionResults) {
29
+ for (let [, result] of sessionResults.runs) {
30
+ for (let errorAction of result.errorLogs) {
31
+ if ("session_error" === errorAction.type) {
32
+ output.push(`\n[${result.webdriverParams.title}:session_error] ${errorAction.error}`);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ function logRunResults(output, result) {
38
+ output.push(`
39
+ ${result.webdriverParams.title}`);
40
+ for (let errorAction of result.errorLogs) {
41
+ if ("log" === errorAction.type) {
42
+ if ("run_error" === errorAction.loggerAction.type) {
43
+ output.push(`${SPACE}[run_error] ${errorAction.loggerAction.error}`);
44
+ }
45
+ }
46
+ }
47
+ if (!result.expectedTests) {
48
+ output.push(` No test runs occured.`);
49
+ }
50
+ // When everything goes right :3
51
+ if (!result.fails &&
52
+ !result.errors &&
53
+ result.expectedTests === result.completedTests &&
54
+ result.expectedModules === result.completedModules &&
55
+ result.expectedCollections === result.completedCollections) {
56
+ output.push(`${SPACE}${result.completedTests} tests
57
+ ${SPACE}${result.completedModules} modules
58
+ ${SPACE}${result.completedCollections} collections`);
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ function logCollectionResult(output, collection) {
64
+ if (!collection)
65
+ return true;
66
+ let { loggerAction } = collection;
67
+ if ("start_collection" !== loggerAction.type)
68
+ return true;
69
+ output.push(`${SPACE}${loggerAction.collection_url}`);
70
+ // when everything in the collection goes right
71
+ if (!collection.fails &&
72
+ !collection.errors &&
73
+ collection.expectedTests === collection.completedTests &&
74
+ collection.expectedModules === collection.completedModules) {
75
+ output.push(`${SPACE.repeat(2)}${collection.expectedTests} tests
76
+ ${SPACE.repeat(2)}${loggerAction.expected_module_count} modules`);
77
+ return true;
78
+ }
79
+ for (let errorAction of collection.errorLogs) {
80
+ if ("collection_error" !== errorAction.type)
81
+ continue;
82
+ output.push(`${SPACE.repeat(2)}[collection_error] ${errorAction.error}`);
83
+ }
84
+ return false;
85
+ }
86
+ function logModuleResult(output, module) {
87
+ if (!module)
88
+ return true;
89
+ let { loggerAction } = module;
90
+ if ("start_module" !== loggerAction.type)
91
+ return true;
92
+ output.push(`${SPACE.repeat(2)}${loggerAction.module_name}`);
93
+ // when everything in the module goes right
94
+ if (!module.fails &&
95
+ !module.errors &&
96
+ module.expectedTests === module.completedTests) {
97
+ output.push(`${SPACE.repeat(3)}${module.expectedTests} tests`);
98
+ return true;
99
+ }
100
+ for (let errorAction of module.errorLogs) {
101
+ if ("collection_error" !== errorAction.type)
102
+ continue;
103
+ output.push(`${SPACE.repeat(2)}[module_error] ${errorAction.error}`);
104
+ }
105
+ return false;
106
+ }
107
+ function logTest(output, test) {
108
+ if (!test)
109
+ return;
110
+ let { loggerStartAction, loggerEndAction } = test;
111
+ if ("start_test" !== loggerStartAction.type)
112
+ return;
113
+ if ("test_error" === loggerEndAction?.type) {
114
+ let { test_name } = loggerStartAction;
115
+ output.push(`${SPACE.repeat(3)}${test_name}
116
+ ${SPACE.repeat(4)}[error] ${loggerEndAction.error}`);
117
+ }
118
+ if ("end_test" === loggerEndAction?.type) {
119
+ let { assertions } = loggerEndAction;
120
+ const isAssertionArray = Array.isArray(assertions) && assertions.length;
121
+ const isAssertion = !Array.isArray(assertions) &&
122
+ undefined !== assertions &&
123
+ null !== assertions;
124
+ if (isAssertion || isAssertionArray) {
125
+ let { test_name } = loggerStartAction;
126
+ output.push(`${SPACE.repeat(3)}${test_name}`);
127
+ }
128
+ if (isAssertion) {
129
+ output.push(`${SPACE.repeat(4)}- ${assertions}`);
130
+ }
131
+ if (isAssertionArray) {
132
+ for (const assertion of assertions) {
133
+ output.push(`${SPACE.repeat(4)}- ${assertion}`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ function logSummary(output, sessionResults) {
139
+ let status_with_color = blue("\u{2714} passed");
140
+ if (sessionResults.fails)
141
+ status_with_color = yellow("\u{2717} failed");
142
+ if (sessionResults.errors)
143
+ status_with_color = gray("\u{2717} errored");
144
+ // expected tests
145
+ let totalTime = 0;
146
+ let testTime = 0;
147
+ for (let [, run] of sessionResults.runs) {
148
+ totalTime += run.endTime - run.startTime;
149
+ testTime += run.testTime;
150
+ }
151
+ output.push(`
152
+ ${status_with_color}
153
+ duration: ${testTime.toFixed(4)} mS
154
+ total: ${totalTime.toFixed(4)} mS
155
+ `);
156
+ }
157
+ // 39 - default foreground color
158
+ // 49 - default background color
159
+ function blue(text) {
160
+ return `\x1b[44m\x1b[97m${text}\x1b[0m`;
161
+ }
162
+ function yellow(text) {
163
+ return `\x1b[43m\x1b[97m${text}\x1b[0m`;
164
+ }
165
+ function gray(text) {
166
+ return `\x1b[100m\x1b[97m${text}\x1b[0m`;
167
+ }
@@ -0,0 +1,172 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { testHanger } from "./test_hangar.js";
4
+ let cwd = process.cwd();
5
+ // better done with URL? feels weird
6
+ let corePath = path.join(import.meta.url.substring(5), "../../../core/dist/");
7
+ let browserPath = path.join(import.meta.url.substring(5), "../../../browser/dist/");
8
+ const MIME_TYPES = {
9
+ octet: "application/octet-stream",
10
+ html: "text/html; charset=UTF-8",
11
+ js: "text/javascript",
12
+ json: "application/json",
13
+ css: "text/css",
14
+ png: "image/png",
15
+ jpg: "image/jpeg",
16
+ ico: "image/x-icon",
17
+ svg: "image/svg+xml",
18
+ };
19
+ export class Router {
20
+ #config;
21
+ #eventbus;
22
+ constructor(config, eventbus) {
23
+ this.#config = config;
24
+ this.#eventbus = eventbus;
25
+ }
26
+ get route() {
27
+ return this.#boundRoute;
28
+ }
29
+ #boundRoute = this.#route.bind(this);
30
+ async #route(req, res) {
31
+ if (serveBadRequest(req, res))
32
+ return;
33
+ if (servePing(req, res))
34
+ return;
35
+ if (serveTestPage(req, res, this.#config))
36
+ return;
37
+ if (logAction(req, res, this.#eventbus))
38
+ return;
39
+ await serveFile(req, res);
40
+ }
41
+ }
42
+ function serveBadRequest(req, res) {
43
+ let { url } = req;
44
+ if (url)
45
+ return false;
46
+ res.setHeader("Content-Type", "text/html");
47
+ res.writeHead(400);
48
+ res.end();
49
+ return true;
50
+ }
51
+ function servePing(req, res) {
52
+ let { url, method } = req;
53
+ if (url !== "/ping" || "GET" !== method)
54
+ return false;
55
+ res.setHeader("Content-Type", "text/html");
56
+ res.writeHead(200);
57
+ res.end("The cookie train has arrived!");
58
+ return true;
59
+ }
60
+ function serveTestPage(req, res, config) {
61
+ let { url, method } = req;
62
+ if (url !== "/" || "GET" !== method)
63
+ return false;
64
+ let hangar = testHanger({
65
+ jackrabbit_url: config.hostAndPort,
66
+ test_collections: process.argv.slice(3),
67
+ });
68
+ res.setHeader("Content-Type", "text/html");
69
+ res.writeHead(200);
70
+ res.end(hangar);
71
+ return true;
72
+ }
73
+ function logAction(req, res, eventbus) {
74
+ let { url, method } = req;
75
+ if (!url?.startsWith("/log/") || "POST" !== method)
76
+ return false;
77
+ let id;
78
+ let cookies = req.headers.cookie?.split(";") ?? [];
79
+ for (const cookieLine of cookies) {
80
+ if (cookieLine.startsWith("jackrabbit=")) {
81
+ let [_name, value] = cookieLine.split("=");
82
+ id = value;
83
+ }
84
+ }
85
+ if (id) {
86
+ getLoggerActionFromRequestBody(req)
87
+ .then(function (loggerAction) {
88
+ eventbus.dispatchAction({
89
+ type: "log",
90
+ loggerAction,
91
+ id,
92
+ });
93
+ res.writeHead(201);
94
+ })
95
+ .catch(function () {
96
+ res.writeHead(401);
97
+ })
98
+ .finally(function () {
99
+ res.end();
100
+ });
101
+ }
102
+ else {
103
+ res.writeHead(401);
104
+ res.end();
105
+ }
106
+ return true;
107
+ }
108
+ async function serveFile(req, res) {
109
+ let { url, method } = req;
110
+ if (!url) {
111
+ res.setHeader("Content-Type", MIME_TYPES["html"]);
112
+ res.writeHead(400);
113
+ res.end();
114
+ return;
115
+ }
116
+ let ext = "";
117
+ if (url.endsWith("/"))
118
+ ext = "index.html";
119
+ let urlNoPrefix = url;
120
+ if (url.startsWith("/jackrabbit"))
121
+ urlNoPrefix = url.substring(11);
122
+ let filePath = path.join(cwd, urlNoPrefix, ext);
123
+ let stream;
124
+ if (url.startsWith("/jackrabbit/core/") && "GET" === method) {
125
+ stream = await getDirectoryScopedFile(filePath, corePath);
126
+ }
127
+ if (url.startsWith("/jackrabbit/browser/") && "GET" === method) {
128
+ stream = await getDirectoryScopedFile(filePath, browserPath);
129
+ }
130
+ if (!url.startsWith("/jackrabbit") && "GET" === method) {
131
+ stream = await getDirectoryScopedFile(filePath, cwd);
132
+ }
133
+ if (stream) {
134
+ // throws errors if not a string
135
+ // filepath is always a string
136
+ const ext = path.extname(filePath).substring(1).toLowerCase();
137
+ let mimeType = MIME_TYPES[ext] ?? MIME_TYPES["octet"];
138
+ res.setHeader("Content-Type", mimeType);
139
+ res.writeHead(200);
140
+ stream.pipe(res);
141
+ }
142
+ else {
143
+ res.setHeader("Content-Type", MIME_TYPES["html"]);
144
+ res.writeHead(404);
145
+ res.end();
146
+ }
147
+ }
148
+ function getLoggerActionFromRequestBody(req) {
149
+ return new Promise(function (resolve, reject) {
150
+ let data = [];
151
+ req.addListener("data", function (chunk) {
152
+ data.push(chunk);
153
+ });
154
+ req.addListener("end", function () {
155
+ let actionStr = Buffer.concat(data).toString();
156
+ let action = JSON.parse(actionStr);
157
+ resolve(action);
158
+ });
159
+ req.addListener("error", function (err) {
160
+ reject(err);
161
+ });
162
+ });
163
+ }
164
+ async function getDirectoryScopedFile(filePath, basePath) {
165
+ if (!filePath.startsWith(basePath))
166
+ return;
167
+ try {
168
+ await fs.promises.access(filePath);
169
+ return fs.createReadStream(filePath);
170
+ }
171
+ catch { }
172
+ }
@@ -0,0 +1,163 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { testHanger } from "./test_hangar.js";
4
+ let cwd = process.cwd();
5
+ // better done with URL? feels weird
6
+ let corePath = path.join(import.meta.url.substring(5), "../../../core/dist/");
7
+ let browserPath = path.join(import.meta.url.substring(5), "../../../browser/dist/");
8
+ const MIME_TYPES = {
9
+ octet: "application/octet-stream",
10
+ html: "text/html; charset=UTF-8",
11
+ js: "text/javascript",
12
+ json: "application/json",
13
+ css: "text/css",
14
+ png: "image/png",
15
+ jpg: "image/jpeg",
16
+ ico: "image/x-icon",
17
+ svg: "image/svg+xml",
18
+ };
19
+ export class Router {
20
+ #config;
21
+ #eventbus;
22
+ constructor(config, eventbus) {
23
+ this.#config = config;
24
+ this.#eventbus = eventbus;
25
+ }
26
+ get route() {
27
+ return this.#boundRoute;
28
+ }
29
+ #boundRoute = this.#route.bind(this);
30
+ async #route(req, res) {
31
+ if (serveBadRequest(req, res))
32
+ return;
33
+ if (servePing(req, res))
34
+ return;
35
+ if (serveTestPage(req, res, this.#config))
36
+ return;
37
+ if (logAction(req, res, this.#eventbus))
38
+ return;
39
+ await serveFile(req, res);
40
+ }
41
+ }
42
+ function getLoggerActionFromRequestBody(req) {
43
+ return new Promise(function (resolve, reject) {
44
+ let data = [];
45
+ req.addListener("data", function (chunk) {
46
+ data.push(chunk);
47
+ });
48
+ req.addListener("end", function () {
49
+ let actionStr = Buffer.concat(data).toString();
50
+ let action = JSON.parse(actionStr);
51
+ resolve(action);
52
+ });
53
+ req.addListener("error", function (err) {
54
+ reject(err);
55
+ });
56
+ });
57
+ }
58
+ async function getDirectoryScopedFile(filePath, basePath) {
59
+ if (!filePath.startsWith(basePath))
60
+ return;
61
+ try {
62
+ await fs.promises.access(filePath);
63
+ return fs.createReadStream(filePath);
64
+ }
65
+ catch { }
66
+ }
67
+ function serveBadRequest(req, res) {
68
+ let { url } = req;
69
+ if (url)
70
+ return false;
71
+ res.setHeader("Content-Type", "text/html");
72
+ res.writeHead(400);
73
+ res.end();
74
+ return true;
75
+ }
76
+ function servePing(req, res) {
77
+ let { url, method } = req;
78
+ if (url !== "/ping" || "GET" !== method)
79
+ return false;
80
+ res.setHeader("Content-Type", "text/html");
81
+ res.writeHead(200);
82
+ res.end("The cookie train has arrived!");
83
+ return true;
84
+ }
85
+ function serveTestPage(req, res, config) {
86
+ let { url, method } = req;
87
+ if (url !== "/" || "GET" !== method)
88
+ return false;
89
+ let hangar = testHanger({
90
+ jackrabbit_url: config.hostAndPort,
91
+ test_collections: process.argv.slice(3),
92
+ });
93
+ res.setHeader("Content-Type", "text/html");
94
+ res.writeHead(200);
95
+ res.end(hangar);
96
+ return true;
97
+ }
98
+ function logAction(req, res, eventbus) {
99
+ let { url, method } = req;
100
+ if (url?.startsWith("/log/") && "POST" === method)
101
+ return false;
102
+ let id;
103
+ let cookies = req.headers.cookie?.split(";") ?? [];
104
+ for (const cookieLine of cookies) {
105
+ if (cookieLine.startsWith("jackrabbit=")) {
106
+ let [_name, value] = cookieLine.split("=");
107
+ id = value;
108
+ }
109
+ }
110
+ if (!id)
111
+ return false;
112
+ getLoggerActionFromRequestBody(req)
113
+ .then(function (loggerAction) {
114
+ eventbus.dispatchAction({
115
+ type: "log",
116
+ loggerAction,
117
+ id,
118
+ });
119
+ res.writeHead(200);
120
+ })
121
+ .catch(function () {
122
+ res.writeHead(403);
123
+ })
124
+ .finally(function () {
125
+ res.end();
126
+ });
127
+ return true;
128
+ }
129
+ async function serveFile(req, res) {
130
+ let { url, method } = req;
131
+ if (!url)
132
+ return;
133
+ let ext = "";
134
+ if (url.endsWith("/"))
135
+ ext = "index.html";
136
+ let urlNoPrefix = url;
137
+ if (url.startsWith("/jackrabbit"))
138
+ urlNoPrefix = url.substring(11);
139
+ let filePath = path.join(cwd, urlNoPrefix, ext);
140
+ let stream;
141
+ if (url.startsWith("/jackrabbit/core/") && "GET" === method) {
142
+ stream = await getDirectoryScopedFile(filePath, corePath);
143
+ }
144
+ if (url.startsWith("/jackrabbit/browser/") && "GET" === method) {
145
+ stream = await getDirectoryScopedFile(filePath, browserPath);
146
+ }
147
+ if (!url.startsWith("/jackrabbit") && "GET" === method) {
148
+ stream = await getDirectoryScopedFile(filePath, cwd);
149
+ }
150
+ if (stream) {
151
+ // throwing errors and stuff
152
+ const ext = path.extname(filePath).substring(1).toLowerCase();
153
+ let mimeType = MIME_TYPES[ext] ?? MIME_TYPES["octet"];
154
+ res.setHeader("Content-Type", mimeType);
155
+ res.writeHead(200);
156
+ stream.pipe(res);
157
+ }
158
+ else {
159
+ res.setHeader("Content-Type", MIME_TYPES["html"]);
160
+ res.writeHead(404);
161
+ res.end();
162
+ }
163
+ }
@@ -0,0 +1,20 @@
1
+ export function testHanger(params) {
2
+ return `<!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <script type="jackrabbit_config">
6
+ ${JSON.stringify(params)}
7
+ </script>
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "jackrabbit/core/": "/jackrabbit/core/"
12
+ }
13
+ }
14
+ </script>
15
+ <script type="module" src="/jackrabbit/browser/dist/mod.js"></script>
16
+ </head>
17
+ <body></body>
18
+ </html>
19
+ `;
20
+ }