@jsenv/core 39.11.2 → 39.13.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 (37) hide show
  1. package/dist/css/directory_listing.css +211 -0
  2. package/dist/html/directory_listing.html +18 -0
  3. package/dist/js/directory_listing.js +240 -0
  4. package/dist/jsenv_core.js +1057 -757
  5. package/dist/other/dir.png +0 -0
  6. package/dist/other/file.png +0 -0
  7. package/dist/other/home.svg +6 -0
  8. package/package.json +6 -6
  9. package/src/build/build.js +7 -7
  10. package/src/build/build_specifier_manager.js +0 -1
  11. package/src/dev/start_dev_server.js +39 -49
  12. package/src/kitchen/kitchen.js +20 -4
  13. package/src/kitchen/out_directory_url.js +2 -1
  14. package/src/kitchen/url_graph/references.js +3 -1
  15. package/src/kitchen/url_graph/url_graph.js +1 -0
  16. package/src/kitchen/url_graph/url_info_transformations.js +37 -4
  17. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
  18. package/src/plugins/plugin_controller.js +170 -114
  19. package/src/plugins/plugins.js +5 -4
  20. package/src/plugins/protocol_file/client/assets/home.svg +6 -0
  21. package/src/plugins/protocol_file/client/directory_listing.css +190 -0
  22. package/src/plugins/protocol_file/client/directory_listing.html +18 -0
  23. package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
  24. package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
  25. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +40 -333
  27. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
  28. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
  29. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
  30. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
  31. package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
  32. package/dist/html/directory.html +0 -165
  33. package/dist/html/html_404_and_ancestor_dir.html +0 -203
  34. package/src/plugins/protocol_file/client/assets/directory.css +0 -133
  35. package/src/plugins/protocol_file/client/directory.html +0 -17
  36. package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
  37. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +0 -37
@@ -3,6 +3,7 @@ import { performance } from "node:perf_hooks";
3
3
  const HOOK_NAMES = [
4
4
  "init",
5
5
  "serve", // is called only during dev/tests
6
+ "serveWebsocket",
6
7
  "resolveReference",
7
8
  "redirectReference",
8
9
  "transformReferenceSearchParams",
@@ -15,6 +16,7 @@ const HOOK_NAMES = [
15
16
  "cooked",
16
17
  "augmentResponse", // is called only during dev/tests
17
18
  "destroy",
19
+ "effect",
18
20
  ];
19
21
 
20
22
  export const createPluginController = (
@@ -28,19 +30,18 @@ export const createPluginController = (
28
30
  return value;
29
31
  };
30
32
 
31
- const plugins = [];
32
- // precompute a list of hooks per hookName for one major reason:
33
- // - When debugging, there is less iteration
34
- // also it should increase perf as there is less work to do
35
- const hookGroups = {};
36
- const addPlugin = (plugin, { position = "end" }) => {
33
+ const pluginCandidates = [];
34
+ const activeEffectSet = new Set();
35
+ const activePlugins = [];
36
+ // precompute a list of hooks per hookName because:
37
+ // 1. [MAJOR REASON] when debugging, there is less iteration (so much better)
38
+ // 2. [MINOR REASON] it should increase perf as there is less work to do
39
+ const hookSetMap = new Map();
40
+ const addPlugin = (plugin, options) => {
37
41
  if (Array.isArray(plugin)) {
38
- if (position === "start") {
39
- plugin = plugin.slice().reverse();
42
+ for (const value of plugin) {
43
+ addPlugin(value, options);
40
44
  }
41
- plugin.forEach((plugin) => {
42
- addPlugin(plugin, { position });
43
- });
44
45
  return;
45
46
  }
46
47
  if (plugin === null || typeof plugin !== "object") {
@@ -50,65 +51,10 @@ export const createPluginController = (
50
51
  plugin.name = "anonymous";
51
52
  }
52
53
  if (!testAppliesDuring(plugin) || !initPlugin(plugin)) {
53
- if (plugin.destroy) {
54
- plugin.destroy();
55
- }
54
+ plugin.destroy?.();
56
55
  return;
57
56
  }
58
- plugins.push(plugin);
59
- for (const key of Object.keys(plugin)) {
60
- if (key === "meta") {
61
- const value = plugin[key];
62
- if (typeof value !== "object" || value === null) {
63
- console.warn(`plugin.meta must be an object, got ${value}`);
64
- continue;
65
- }
66
- Object.assign(pluginsMeta, value);
67
- // any extension/modification on plugin.meta
68
- // won't be taken into account so we freeze object
69
- // to throw in case it happen
70
- Object.freeze(value);
71
- continue;
72
- }
73
-
74
- if (
75
- key === "name" ||
76
- key === "appliesDuring" ||
77
- key === "init" ||
78
- key === "serverEvents" ||
79
- key === "mustStayFirst"
80
- ) {
81
- continue;
82
- }
83
- const isHook = HOOK_NAMES.includes(key);
84
- if (!isHook) {
85
- console.warn(`Unexpected "${key}" property on "${plugin.name}" plugin`);
86
- continue;
87
- }
88
- const hookName = key;
89
- const hookValue = plugin[hookName];
90
- if (hookValue) {
91
- const group = hookGroups[hookName] || (hookGroups[hookName] = []);
92
- const hook = {
93
- plugin,
94
- name: hookName,
95
- value: hookValue,
96
- };
97
- if (position === "start") {
98
- let i = 0;
99
- while (i < group.length) {
100
- const before = group[i];
101
- if (!before.plugin.mustStayFirst) {
102
- break;
103
- }
104
- i++;
105
- }
106
- group.splice(i, 0, hook);
107
- } else {
108
- group.push(hook);
109
- }
110
- }
111
- }
57
+ pluginCandidates.push(plugin);
112
58
  };
113
59
  const testAppliesDuring = (plugin) => {
114
60
  const { appliesDuring } = plugin;
@@ -147,22 +93,131 @@ export const createPluginController = (
147
93
  );
148
94
  };
149
95
  const initPlugin = (plugin) => {
150
- if (plugin.init) {
151
- const initReturnValue = plugin.init(kitchenContext, plugin);
152
- if (initReturnValue === false) {
153
- return false;
154
- }
155
- if (typeof initReturnValue === "function" && !plugin.destroy) {
156
- plugin.destroy = initReturnValue;
157
- }
96
+ const { init } = plugin;
97
+ if (!init) {
98
+ return true;
99
+ }
100
+ const initReturnValue = init(kitchenContext, { plugin });
101
+ if (initReturnValue === false) {
102
+ return false;
103
+ }
104
+ if (typeof initReturnValue === "function" && !plugin.destroy) {
105
+ plugin.destroy = initReturnValue;
158
106
  }
159
107
  return true;
160
108
  };
161
- const pushPlugin = (plugin) => {
162
- addPlugin(plugin, { position: "end" });
109
+ const pushPlugin = (...args) => {
110
+ for (const arg of args) {
111
+ addPlugin(arg);
112
+ }
113
+ updateActivePlugins();
163
114
  };
164
- const unshiftPlugin = (plugin) => {
165
- addPlugin(plugin, { position: "start" });
115
+ const updateActivePlugins = () => {
116
+ // construct activePlugins and hooks according
117
+ // to the one present in candidates and their effects
118
+ // 1. active plugins is an empty array
119
+ // 2. all active effects are cleaned-up
120
+ // 3. all effects are re-activated if still relevant
121
+ // 4. hooks are precomputed according to plugin order
122
+
123
+ // 1.
124
+ activePlugins.length = 0;
125
+ // 2.
126
+ for (const { cleanup } of activeEffectSet) {
127
+ cleanup();
128
+ }
129
+ activeEffectSet.clear();
130
+ for (const pluginCandidate of pluginCandidates) {
131
+ const effect = pluginCandidate.effect;
132
+ if (!effect) {
133
+ activePlugins.push(pluginCandidate);
134
+ continue;
135
+ }
136
+ }
137
+ // 3.
138
+ for (const pluginCandidate of pluginCandidates) {
139
+ const effect = pluginCandidate.effect;
140
+ if (!effect) {
141
+ continue;
142
+ }
143
+ const returnValue = effect({
144
+ kitchenContext,
145
+ otherPlugins: activePlugins,
146
+ });
147
+ if (!returnValue) {
148
+ continue;
149
+ }
150
+ activePlugins.push(pluginCandidate);
151
+ activeEffectSet.add({
152
+ plugin: pluginCandidate,
153
+ cleanup: typeof returnValue === "function" ? returnValue : () => {},
154
+ });
155
+ }
156
+ // 4.
157
+ activePlugins.sort((a, b) => {
158
+ return pluginCandidates.indexOf(a) - pluginCandidates.indexOf(b);
159
+ });
160
+ hookSetMap.clear();
161
+ for (const activePlugin of activePlugins) {
162
+ for (const key of Object.keys(activePlugin)) {
163
+ if (key === "meta") {
164
+ const value = activePlugin[key];
165
+ if (typeof value !== "object" || value === null) {
166
+ console.warn(`plugin.meta must be an object, got ${value}`);
167
+ continue;
168
+ }
169
+ Object.assign(pluginsMeta, value);
170
+ // any extension/modification on plugin.meta
171
+ // won't be taken into account so we freeze object
172
+ // to throw in case it happen
173
+ Object.freeze(value);
174
+ continue;
175
+ }
176
+ if (
177
+ key === "name" ||
178
+ key === "appliesDuring" ||
179
+ key === "init" ||
180
+ key === "serverEvents" ||
181
+ key === "mustStayFirst" ||
182
+ key === "effect"
183
+ ) {
184
+ continue;
185
+ }
186
+ const isHook = HOOK_NAMES.includes(key);
187
+ if (!isHook) {
188
+ console.warn(
189
+ `Unexpected "${key}" property on "${activePlugin.name}" plugin`,
190
+ );
191
+ continue;
192
+ }
193
+ const hookName = key;
194
+ const hookValue = activePlugin[hookName];
195
+ if (hookValue) {
196
+ let hookSet = hookSetMap.get(hookName);
197
+ if (!hookSet) {
198
+ hookSet = new Set();
199
+ hookSetMap.set(hookName, hookSet);
200
+ }
201
+ const hook = {
202
+ plugin: activePlugin,
203
+ name: hookName,
204
+ value: hookValue,
205
+ };
206
+ // if (position === "start") {
207
+ // let i = 0;
208
+ // while (i < group.length) {
209
+ // const before = group[i];
210
+ // if (!before.plugin.mustStayFirst) {
211
+ // break;
212
+ // }
213
+ // i++;
214
+ // }
215
+ // group.splice(i, 0, hook);
216
+ // } else {
217
+ hookSet.add(hook);
218
+ }
219
+ }
220
+ }
166
221
  };
167
222
 
168
223
  let lastPluginUsed = null;
@@ -215,75 +270,76 @@ export const createPluginController = (
215
270
  };
216
271
 
217
272
  const callHooks = (hookName, info, callback) => {
218
- const hooks = hookGroups[hookName];
219
- if (hooks) {
220
- const setHookParams = (firstArg = info) => {
221
- info = firstArg;
222
- };
223
- for (const hook of hooks) {
224
- const returnValue = callHook(hook, info);
225
- if (returnValue && callback) {
226
- callback(returnValue, hook.plugin, setHookParams);
227
- }
273
+ const hookSet = hookSetMap.get(hookName);
274
+ if (!hookSet) {
275
+ return;
276
+ }
277
+ const setHookParams = (firstArg = info) => {
278
+ info = firstArg;
279
+ };
280
+ for (const hook of hookSet) {
281
+ const returnValue = callHook(hook, info);
282
+ if (returnValue && callback) {
283
+ callback(returnValue, hook.plugin, setHookParams);
228
284
  }
229
285
  }
230
286
  };
231
287
  const callAsyncHooks = async (hookName, info, callback, options) => {
232
- const hooks = hookGroups[hookName];
233
- if (hooks) {
234
- for (const hook of hooks) {
235
- const returnValue = await callAsyncHook(hook, info, options);
236
- if (returnValue && callback) {
237
- await callback(returnValue, hook.plugin);
238
- }
288
+ const hookSet = hookSetMap.get(hookName);
289
+ if (!hookSet) {
290
+ return;
291
+ }
292
+ for (const hook of hookSet) {
293
+ const returnValue = await callAsyncHook(hook, info, options);
294
+ if (returnValue && callback) {
295
+ await callback(returnValue, hook.plugin);
239
296
  }
240
297
  }
241
298
  };
242
299
 
243
300
  const callHooksUntil = (hookName, info) => {
244
- const hooks = hookGroups[hookName];
245
- if (hooks) {
246
- for (const hook of hooks) {
247
- const returnValue = callHook(hook, info);
248
- if (returnValue) {
249
- return returnValue;
250
- }
301
+ const hookSet = hookSetMap.get(hookName);
302
+ if (!hookSet) {
303
+ return null;
304
+ }
305
+ for (const hook of hookSet) {
306
+ const returnValue = callHook(hook, info);
307
+ if (returnValue) {
308
+ return returnValue;
251
309
  }
252
310
  }
253
311
  return null;
254
312
  };
255
313
  const callAsyncHooksUntil = async (hookName, info, options) => {
256
- const hooks = hookGroups[hookName];
257
- if (!hooks) {
314
+ const hookSet = hookSetMap.get(hookName);
315
+ if (!hookSet) {
258
316
  return null;
259
317
  }
260
- if (hooks.length === 0) {
318
+ if (hookSet.size === 0) {
261
319
  return null;
262
320
  }
321
+ const iterator = hookSet.values()[Symbol.iterator]();
263
322
  let result;
264
- let index = 0;
265
323
  const visit = async () => {
266
- if (index >= hooks.length) {
324
+ const { done, value: hook } = iterator.next();
325
+ if (done) {
267
326
  return;
268
327
  }
269
- const hook = hooks[index];
270
328
  const returnValue = await callAsyncHook(hook, info, options);
271
329
  if (returnValue) {
272
330
  result = returnValue;
273
331
  return;
274
332
  }
275
- index++;
276
333
  await visit();
277
334
  };
278
- await visit(0);
335
+ await visit();
279
336
  return result;
280
337
  };
281
338
 
282
339
  return {
283
340
  pluginsMeta,
284
- plugins,
341
+ activePlugins,
285
342
  pushPlugin,
286
- unshiftPlugin,
287
343
  getHookFunction,
288
344
  callHook,
289
345
  callAsyncHook,
@@ -65,8 +65,8 @@ export const getCorePlugins = ({
65
65
  jsenvPluginReferenceAnalysis(referenceAnalysis),
66
66
  ...(injections ? [jsenvPluginInjections(injections)] : []),
67
67
  jsenvPluginTranspilation(transpilation),
68
+ // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
68
69
  ...(inlining ? [jsenvPluginInlining()] : []),
69
- ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
70
70
 
71
71
  /* When resolving references the following applies by default:
72
72
  - http urls are resolved by jsenvPluginHttpUrls
@@ -80,7 +80,6 @@ export const getCorePlugins = ({
80
80
  magicDirectoryIndex,
81
81
  directoryListingUrlMocks,
82
82
  }),
83
-
84
83
  {
85
84
  name: "jsenv:resolve_root_as_main",
86
85
  appliesDuring: "*",
@@ -99,12 +98,14 @@ export const getCorePlugins = ({
99
98
  : []),
100
99
  jsenvPluginWebResolution(),
101
100
  jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
102
-
103
101
  jsenvPluginVersionSearchParam(),
102
+
103
+ // "jsenvPluginSupervisor" MUST be after "jsenvPluginInlining" as it needs inline script to be cooked
104
+ ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
105
+
104
106
  jsenvPluginCommonJsGlobals(),
105
107
  jsenvPluginImportMetaScenarios(),
106
108
  ...(scenarioPlaceholders ? [jsenvPluginGlobalScenarios()] : []),
107
-
108
109
  jsenvPluginNodeRuntime({ runtimeCompat }),
109
110
 
110
111
  jsenvPluginImportMetaHot(),
@@ -0,0 +1,6 @@
1
+ <svg id="root" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="30.4 23.47 40.05 45.63">
2
+ <path d="M32.5,69.1c-0.6,0-1.1-0.2-1.4-0.5c-0.7-0.6-0.7-1.5-0.7-1.8V41.1h4v24h9v4H32.9C32.8,69.1,32.6,69.1,32.5,69.1z" />
3
+ <path d="M67.8,69.1c-0.2,0-0.3,0-0.5,0c-0.1,0-0.1,0-0.2,0H56.4v-4h10v-23h4v24.7c0,0.6-0.3,1.2-0.7,1.7 C69.1,69,68.3,69.1,67.8,69.1z" />
4
+ <path d="M68.4,44.1c-0.5,0-1-0.2-1.3-0.5L50,28.2L33.7,42.6c-0.8,0.7-2.1,0.7-2.8-0.2c-0.7-0.8-0.7-2.1,0.2-2.8L48.7,24 c0.8-0.7,1.9-0.7,2.7,0l18.4,16.6c0.8,0.7,0.9,2,0.1,2.8C69.5,43.8,68.9,44.1,68.4,44.1z" />
5
+ <path d="M58.4,69.1h-4v-13h-8v13h-4v-15c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2V69.1z" />
6
+ </svg>
@@ -0,0 +1,190 @@
1
+ html,
2
+ body,
3
+ h1,
4
+ div,
5
+ p,
6
+ ul,
7
+ li,
8
+ a,
9
+ p,
10
+ button {
11
+ margin: 0;
12
+ padding: 0;
13
+ border: 0;
14
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
15
+ -webkit-font-smoothing: antialiased;
16
+ -moz-osx-font-smoothing: grayscale;
17
+ }
18
+
19
+ /* ERROR MESSAGE */
20
+ .error_message {
21
+ background-color: #fce4e4;
22
+ border: 1px solid #fcc2c3;
23
+ padding: 1.5em 1.2em;
24
+ margin: 1em;
25
+ display: block;
26
+ }
27
+ .error_text {
28
+ color: #853611;
29
+ font-weight: bold;
30
+ text-shadow: 1px 1px rgba(250, 250, 250, 0.3);
31
+ }
32
+ .hint_message {
33
+ background-color: lightblue;
34
+ border: 1px solid #fcc2c3;
35
+ padding: 1.5em 1.2em;
36
+ margin: 1em;
37
+ display: block;
38
+ }
39
+ .hint_text {
40
+ color: black;
41
+ font-weight: bold;
42
+ line-height: 2em;
43
+ text-shadow: 1px 1px rgba(250, 250, 250, 0.3);
44
+ }
45
+
46
+ /* NAV */
47
+ .nav {
48
+ font-size: 16px;
49
+ font-weight: bold;
50
+ margin: 20px 25px 15px 25px;
51
+ display: flex;
52
+ gap: 0.3em;
53
+ }
54
+ .nav_item {
55
+ display: flex;
56
+ align-items: center;
57
+ }
58
+ .nav_item_icon {
59
+ display: flex;
60
+ width: 1em;
61
+ height: 1em;
62
+ margin-right: 0.25em;
63
+ color: #f1f1f1;
64
+ }
65
+ a.nav_item_icon {
66
+ text-decoration: none;
67
+ }
68
+ .nav_item_text {
69
+ position: relative;
70
+ color: inherit;
71
+ }
72
+ a.nav_item_text {
73
+ text-decoration: none;
74
+ }
75
+ .nav_item[data-404] .nav_item_text {
76
+ background-color: yellow;
77
+ color: #853611;
78
+ }
79
+ .file_path_bad {
80
+ text-decoration-line: underline;
81
+ text-decoration-color: red;
82
+ text-decoration-style: wavy;
83
+ }
84
+ .nav_item_badge_404 {
85
+ color: #853611;
86
+ text-align: center;
87
+ background-color: yellow;
88
+ border-radius: 5px;
89
+ padding: 0.2em 0.1em;
90
+ font-size: 0.8em;
91
+ margin-right: 0.3em;
92
+ }
93
+
94
+ /* CONTENT */
95
+ .directory_empty_message {
96
+ margin: 1em;
97
+ padding: 0.5em;
98
+ color: #bbbbbb;
99
+ }
100
+ .directory_content {
101
+ margin: 10px 15px 10px 15px;
102
+ list-style-type: none;
103
+ border-radius: 3px;
104
+ }
105
+ .directory_content_item {
106
+ display: flex;
107
+ position: relative;
108
+ padding: 10px 15px 10px 15px;
109
+ font-size: 15px;
110
+ min-height: 19px;
111
+ box-sizing: border-box;
112
+ align-items: center;
113
+ }
114
+ .directory_content_item_link {
115
+ display: flex;
116
+ flex: 1;
117
+ gap: 10px;
118
+ align-items: center;
119
+ text-decoration: none;
120
+ }
121
+ .directory_content_item_icon {
122
+ width: 15px;
123
+ aspect-ratio: 1/1;
124
+ display: flex;
125
+ color: #f1f1f1;
126
+ }
127
+ .directory_content_item_icon img {
128
+ width: 100%;
129
+ }
130
+ .directory_content_item_text {
131
+ display: flex;
132
+ }
133
+ .directory_content_item:last-child {
134
+ border-bottom: none;
135
+ padding-bottom: 10px;
136
+ }
137
+ .directory_content_item_arrow {
138
+ display: flex;
139
+ color: #666666;
140
+ height: 10px;
141
+ stroke-width: 15%;
142
+ }
143
+
144
+ body[data-theme="dark"] {
145
+ background: #121212;
146
+ }
147
+ body[data-theme="light"] {
148
+ background: #f1f1f1;
149
+ }
150
+ body[data-theme="dark"] .nav_item {
151
+ color: #999999;
152
+ }
153
+ body[data-theme="dark"] .nav_item[data-current] {
154
+ color: #f1f1f1;
155
+ }
156
+ body[data-theme="light"] .nav_item {
157
+ color: #000000;
158
+ }
159
+ body[data-theme="dark"] .directory_separator {
160
+ color: #666666;
161
+ }
162
+ body[data-theme="light"] .directory_separator {
163
+ color: #999999;
164
+ }
165
+ body[data-theme="dark"] .directory_content {
166
+ background: #1e1e1e;
167
+ }
168
+ body[data-theme="light"] .directory_content {
169
+ background: #fefefe;
170
+ }
171
+ body[data-theme="dark"] .directory_content_item {
172
+ border-bottom: 1px solid #121212;
173
+ }
174
+ body[data-theme="light"] .directory_content_item {
175
+ border-bottom: 1px solid #f1f1f1;
176
+ }
177
+ body[data-theme="dark"] .directory_content_item[data-type="dir"]::before {
178
+ border-top: 2px solid #666666;
179
+ border-right: 2px solid #666666;
180
+ }
181
+ body[data-theme="light"] .directory_content_item[data-type="file"]::before {
182
+ border-top: 2px solid #999999;
183
+ border-right: 2px solid #999999;
184
+ }
185
+ body[data-theme="dark"] .directory_content_item a {
186
+ color: #ffffff;
187
+ }
188
+ body[data-theme="light"] .directory_content_item a {
189
+ color: #000000;
190
+ }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Directory explorer</title>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon" href="data:," />
7
+ <link rel="stylesheet" href="./directory_listing.css" />
8
+ </head>
9
+
10
+ <body data-theme="dark">
11
+ <div id="root"></div>
12
+ <script>
13
+ // eslint-disable-next-line no-undef
14
+ window.DIRECTORY_LISTING = __DIRECTORY_LISTING__;
15
+ </script>
16
+ <script type="module" src="./directory_listing.jsx"></script>
17
+ </body>
18
+ </html>