@jsenv/core 23.11.1 → 24.1.0-alpha.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.
@@ -50,6 +50,7 @@ export const compileHtml = async ({
50
50
 
51
51
  jsenvScriptInjection = true,
52
52
  jsenvToolbarInjection,
53
+ onHtmlImportmapInfo,
53
54
  }) => {
54
55
  const jsenvBrowserBuildUrlRelativeToProject = urlToRelativeUrl(
55
56
  jsenvBrowserSystemFileInfo.jsenvBuildUrl,
@@ -86,55 +87,127 @@ export const compileHtml = async ({
86
87
  ],
87
88
  })
88
89
 
90
+ let sources = []
91
+ let sourcesContent = []
89
92
  const { scripts } = parseHtmlAstRessources(htmlAst)
90
- const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
91
-
92
- let hasImportmap = false
93
- const inlineScriptsContentMap = {}
94
- const importmapsToInline = []
93
+ let importmapInfo = null
95
94
  scripts.forEach((script) => {
96
95
  const typeAttribute = getHtmlNodeAttributeByName(script, "type")
97
- const srcAttribute = getHtmlNodeAttributeByName(script, "src")
98
-
99
- // importmap
100
96
  if (typeAttribute && typeAttribute.value === "importmap") {
101
- hasImportmap = true
102
-
103
- if (srcAttribute) {
104
- if (moduleOutFormat === "systemjs") {
105
- typeAttribute.value = "jsenv-importmap"
106
- return // no need to inline
97
+ if (importmapInfo) {
98
+ console.error("HTML file must contain max 1 importmap")
99
+ } else {
100
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
101
+ const src = srcAttribute ? srcAttribute.value : ""
102
+ if (src) {
103
+ importmapInfo = {
104
+ script,
105
+ url: resolveUrl(src, url),
106
+ loadAsText: async () => {
107
+ const importMapResponse = await fetchUrl(importmapInfo.url)
108
+ if (importMapResponse.status !== 200) {
109
+ logger.warn(
110
+ createDetailedMessage(
111
+ importMapResponse.status === 404
112
+ ? `importmap script file cannot be found.`
113
+ : `importmap script file unexpected response status (${importMapResponse.status}).`,
114
+ {
115
+ "importmap url": importmapInfo.url,
116
+ "html url": url,
117
+ },
118
+ ),
119
+ )
120
+ return "{}"
121
+ }
122
+ const importmapAsText = await importMapResponse.text()
123
+ sources.push(importmapInfo.url)
124
+ sourcesContent.push(importmapAsText)
125
+
126
+ const importMapMoved = moveImportMap(
127
+ JSON.parse(importmapAsText),
128
+ importmapInfo.url,
129
+ url,
130
+ )
131
+ const compiledImportmapAsText = JSON.stringify(
132
+ importMapMoved,
133
+ null,
134
+ " ",
135
+ )
136
+ return compiledImportmapAsText
137
+ },
138
+ }
139
+ } else {
140
+ importmapInfo = {
141
+ script,
142
+ url: compiledUrl,
143
+ loadAsText: () => getHtmlNodeTextNode(script).value,
144
+ }
107
145
  }
108
-
109
- // we force inline because browsers supporting importmap supports only when they are inline
110
- importmapsToInline.push({
111
- script,
112
- src: srcAttribute.value,
113
- })
114
- return
115
- }
116
-
117
- const defaultImportMap = getDefaultImportMap({
118
- importMapFileUrl: compiledUrl,
119
- projectDirectoryUrl,
120
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
121
- })
122
- const inlineImportMap = JSON.parse(getHtmlNodeTextNode(script).value)
123
- const mappings = composeTwoImportMaps(defaultImportMap, inlineImportMap)
124
- if (moduleOutFormat === "systemjs") {
125
- typeAttribute.value = "jsenv-importmap"
126
146
  }
127
- setHtmlNodeText(script, JSON.stringify(mappings, null, " "))
128
- return
129
147
  }
148
+ })
149
+ if (importmapInfo) {
150
+ const htmlImportMap = JSON.parse(await importmapInfo.loadAsText())
151
+ const importMapFromJsenv = getDefaultImportMap({
152
+ importMapFileUrl: compiledUrl,
153
+ projectDirectoryUrl,
154
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
155
+ })
156
+ const mappings = composeTwoImportMaps(importMapFromJsenv, htmlImportMap)
157
+ const importmapAsText = JSON.stringify(mappings, null, " ")
158
+ replaceHtmlNode(
159
+ importmapInfo.script,
160
+ `<script type="${
161
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap"
162
+ }">${importmapAsText}</script>`,
163
+ {
164
+ attributesToIgnore: ["src"],
165
+ },
166
+ )
167
+ importmapInfo.inlinedFrom = importmapInfo.url
168
+ importmapInfo.url = compiledUrl
169
+ importmapInfo.text = importmapAsText
170
+ } else {
171
+ // inject a default importmap
172
+ const defaultImportMap = getDefaultImportMap({
173
+ importMapFileUrl: compiledUrl,
174
+ projectDirectoryUrl,
175
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
176
+ })
177
+ const importmapAsText = JSON.stringify(defaultImportMap, null, " ")
178
+ manipulateHtmlAst(htmlAst, {
179
+ scriptInjections: [
180
+ {
181
+ type:
182
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
183
+ // in case there is no importmap, force the presence
184
+ // so that '@jsenv/core/' are still remapped
185
+ text: importmapAsText,
186
+ },
187
+ ],
188
+ })
189
+ importmapInfo = {
190
+ url: compiledUrl,
191
+ text: importmapAsText,
192
+ }
193
+ }
194
+ onHtmlImportmapInfo({
195
+ htmlUrl: url,
196
+ importmapInfo,
197
+ })
130
198
 
199
+ const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
200
+ const inlineScriptsContentMap = {}
201
+ scripts.forEach((script) => {
202
+ const typeAttribute = getHtmlNodeAttributeByName(script, "type")
203
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
204
+ const src = srcAttribute ? srcAttribute.value : ""
131
205
  // remote module script
132
- if (typeAttribute && typeAttribute.value === "module" && srcAttribute) {
206
+ if (typeAttribute && typeAttribute.value === "module" && src) {
133
207
  if (moduleOutFormat === "systemjs") {
134
208
  removeHtmlNodeAttribute(script, typeAttribute)
135
209
  }
136
210
  removeHtmlNodeAttribute(script, srcAttribute)
137
- const src = srcAttribute.value
138
211
  const jsenvMethod =
139
212
  moduleOutFormat === "systemjs"
140
213
  ? "executeFileUsingSystemJs"
@@ -174,66 +247,6 @@ export const compileHtml = async ({
174
247
  return
175
248
  }
176
249
  })
177
-
178
- if (hasImportmap === false) {
179
- const defaultImportMap = getDefaultImportMap({
180
- importMapFileUrl: compiledUrl,
181
- projectDirectoryUrl,
182
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
183
- })
184
- manipulateHtmlAst(htmlAst, {
185
- scriptInjections: [
186
- {
187
- type:
188
- moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
189
- // in case there is no importmap, force the presence
190
- // so that '@jsenv/core/' are still remapped
191
- text: JSON.stringify(defaultImportMap, null, " "),
192
- },
193
- ],
194
- })
195
- }
196
-
197
- await Promise.all(
198
- importmapsToInline.map(async ({ script, src }) => {
199
- const importMapUrl = resolveUrl(src, url)
200
- const importMapResponse = await fetchUrl(importMapUrl)
201
- if (importMapResponse.status !== 200) {
202
- logger.warn(
203
- createDetailedMessage(
204
- importMapResponse.status === 404
205
- ? `Cannot inline importmap script because file cannot be found.`
206
- : `Cannot inline importmap script due to unexpected response status (${importMapResponse.status}).`,
207
- {
208
- "importmap script src": src,
209
- "importmap url": importMapUrl,
210
- "html url": url,
211
- },
212
- ),
213
- )
214
- return
215
- }
216
-
217
- const importMapContent = await importMapResponse.json()
218
- const importMapInlined = moveImportMap(
219
- importMapContent,
220
- importMapUrl,
221
- url,
222
- )
223
- replaceHtmlNode(
224
- script,
225
- `<script type="importmap">${JSON.stringify(
226
- importMapInlined,
227
- null,
228
- " ",
229
- )}</script>`,
230
- {
231
- attributesToIgnore: ["src"],
232
- },
233
- )
234
- }),
235
- )
236
-
237
250
  const htmlAfterTransformation = stringifyHtmlAst(htmlAst)
238
251
 
239
252
  let assets = []
@@ -303,12 +316,14 @@ export const compileHtml = async ({
303
316
  }
304
317
  }),
305
318
  )
319
+ sources.push(url)
320
+ sourcesContent.push(code)
306
321
 
307
322
  return {
308
323
  contentType: "text/html",
309
324
  compiledSource: htmlAfterTransformation,
310
- sources: [url],
311
- sourcesContent: [code],
325
+ sources,
326
+ sourcesContent,
312
327
  assets,
313
328
  assetsContent,
314
329
  dependencies: htmlDependencies.map(({ specifier }) => {
@@ -50,7 +50,15 @@ import { createTransformHtmlSourceFileService } from "./html_source_file_service
50
50
  export const startCompileServer = async ({
51
51
  signal = new AbortController().signal,
52
52
  handleSIGINT,
53
- compileServerLogLevel,
53
+ logLevel,
54
+ protocol = "http",
55
+ http2 = protocol === "https",
56
+ privateKey,
57
+ certificate,
58
+ ip = "0.0.0.0",
59
+ port = 0,
60
+ keepProcessAlive = false,
61
+ onStop = () => {},
54
62
 
55
63
  projectDirectoryUrl,
56
64
 
@@ -82,16 +90,6 @@ export const startCompileServer = async ({
82
90
  babelConfigFileUrl,
83
91
  customCompilers = {},
84
92
 
85
- // options related to the server itself
86
- compileServerProtocol = "http",
87
- compileServerHttp2 = compileServerProtocol === "https",
88
- compileServerPrivateKey,
89
- compileServerCertificate,
90
- compileServerIp = "0.0.0.0",
91
- compileServerPort = 0,
92
- keepProcessAlive = false,
93
- onStop = () => {},
94
-
95
93
  // remaining options
96
94
  runtimeSupport,
97
95
 
@@ -103,7 +101,7 @@ export const startCompileServer = async ({
103
101
  },
104
102
  livereloadLogLevel = "info",
105
103
  customServices = {},
106
- serverPlugins,
104
+ plugins,
107
105
  livereloadSSE = false,
108
106
  transformHtmlSourceFiles = true,
109
107
  jsenvToolbarInjection = false,
@@ -134,7 +132,7 @@ export const startCompileServer = async ({
134
132
  projectDirectoryUrl,
135
133
  )
136
134
 
137
- const logger = createLogger({ logLevel: compileServerLogLevel })
135
+ const logger = createLogger({ logLevel })
138
136
 
139
137
  const browser = isBrowserPartOfSupportedRuntimes(runtimeSupport)
140
138
  const babelPluginMapFromFile = await loadBabelPluginMapFromFile({
@@ -241,14 +239,14 @@ export const startCompileServer = async ({
241
239
  })
242
240
  customServices = {
243
241
  ...customServices,
244
- "service:sse": serveSSEForLivereload,
242
+ "jsenv:sse": serveSSEForLivereload,
245
243
  }
246
244
  } else {
247
245
  const roomWhenLivereloadIsDisabled = createSSERoom()
248
246
  roomWhenLivereloadIsDisabled.open()
249
247
  customServices = {
250
248
  ...customServices,
251
- "service:sse": (request) => {
249
+ "jsenv:sse": (request) => {
252
250
  const { accept } = request.headers
253
251
  if (!accept || !accept.includes("text/event-stream")) {
254
252
  return null
@@ -350,16 +348,16 @@ export const startCompileServer = async ({
350
348
  sendServerInternalErrorDetails: true,
351
349
  keepProcessAlive,
352
350
 
353
- logLevel: compileServerLogLevel,
351
+ logLevel,
354
352
 
355
- protocol: compileServerProtocol,
356
- http2: compileServerHttp2,
357
- serverCertificate: compileServerCertificate,
358
- serverCertificatePrivateKey: compileServerPrivateKey,
359
- ip: compileServerIp,
360
- port: compileServerPort,
353
+ protocol,
354
+ http2,
355
+ certificate,
356
+ privateKey,
357
+ ip,
358
+ port,
361
359
  plugins: {
362
- ...serverPlugins,
360
+ ...plugins,
363
361
  ...pluginCORS({
364
362
  accessControlAllowRequestOrigin: true,
365
363
  accessControlAllowRequestMethod: true,
@@ -391,6 +389,7 @@ export const startCompileServer = async ({
391
389
  ...compileServer,
392
390
  compileServerGroupMap,
393
391
  babelPluginMap,
392
+ projectFileRequestedCallback,
394
393
  }
395
394
  }
396
395
 
@@ -584,6 +583,9 @@ const setupServerSentEventsForLivereload = ({
584
583
  const projectFileAdded = createCallbackList()
585
584
 
586
585
  const projectFileRequestedCallback = (relativeUrl, request) => {
586
+ if (relativeUrl[0] === "/") {
587
+ relativeUrl = relativeUrl.slice(1)
588
+ }
587
589
  const url = `${projectDirectoryUrl}${relativeUrl}`
588
590
 
589
591
  if (
@@ -629,62 +631,82 @@ const setupServerSentEventsForLivereload = ({
629
631
  clearTimeout(timeout)
630
632
  })
631
633
 
632
- const getDependencySet = (mainRelativeUrl) => {
633
- if (trackerMap.has(mainRelativeUrl)) {
634
- return trackerMap.get(mainRelativeUrl)
634
+ const startTrackingRoot = (rootFile) => {
635
+ stopTrackingRoot(rootFile)
636
+ const set = new Set()
637
+ set.add(rootFile)
638
+ const depInfo = {
639
+ set,
640
+ cleanup: [],
635
641
  }
636
- const dependencySet = new Set()
637
- dependencySet.add(mainRelativeUrl)
638
- trackerMap.set(mainRelativeUrl, dependencySet)
639
- return dependencySet
642
+ trackerMap.set(rootFile, depInfo)
643
+ return depInfo
644
+ }
645
+ const addStopTrackingCalback = (rootFile, callback) => {
646
+ trackerMap.get(rootFile).cleanup.push(callback)
647
+ }
648
+ const stopTrackingRoot = (rootFile) => {
649
+ const depInfo = trackerMap.get(rootFile)
650
+ if (depInfo) {
651
+ depInfo.cleanup.forEach((cb) => {
652
+ cb()
653
+ })
654
+ trackerMap.delete(rootFile)
655
+ }
656
+ }
657
+ const isDependencyOf = (file, rootFile) => {
658
+ const depInfo = trackerMap.get(rootFile)
659
+ return depInfo && depInfo.set.has(file)
660
+ }
661
+ const markAsDependencyOf = (file, rootFile) => {
662
+ trackerMap.get(rootFile).set.add(file)
640
663
  }
641
664
 
642
665
  // each time a file is requested for the first time its dependencySet is computed
643
- projectFileRequested.add(({ relativeUrl: mainRelativeUrl }) => {
666
+ projectFileRequested.add((requestInfo) => {
667
+ const rootRelativeUrl = requestInfo.relativeUrl
644
668
  // for now no use case of livereloading on node.js
645
669
  // and for browsers only html file can be main files
646
670
  // this avoid collecting dependencies of non html files that will never be used
647
- if (!mainRelativeUrl.endsWith(".html")) {
671
+ if (!rootRelativeUrl.endsWith(".html")) {
648
672
  return
649
673
  }
650
674
 
675
+ livereloadLogger.debug(`${rootRelativeUrl} requested -> start tracking it`)
651
676
  // when a file is requested, always rebuild its dependency in case it has changed
652
677
  // since the last time it was requested
653
- const dependencySet = new Set()
654
- dependencySet.add(mainRelativeUrl)
655
- trackerMap.set(mainRelativeUrl, dependencySet)
678
+ startTrackingRoot(rootRelativeUrl)
656
679
 
657
680
  const removeDependencyRequestedCallback = projectFileRequested.add(
658
681
  ({ relativeUrl, request }) => {
659
- if (dependencySet.has(relativeUrl)) {
682
+ if (isDependencyOf(relativeUrl, rootRelativeUrl)) {
660
683
  return
661
684
  }
662
-
663
685
  const dependencyReport = reportDependency(
664
686
  relativeUrl,
665
- mainRelativeUrl,
687
+ rootRelativeUrl,
666
688
  request,
667
689
  )
668
690
  if (dependencyReport.dependency === false) {
669
691
  livereloadLogger.debug(
670
- `${relativeUrl} not a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
692
+ `${relativeUrl} not a dependency of ${rootRelativeUrl} because ${dependencyReport.reason}`,
671
693
  )
672
694
  return
673
695
  }
674
-
675
696
  livereloadLogger.debug(
676
- `${relativeUrl} is a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
697
+ `${relativeUrl} is a dependency of ${rootRelativeUrl} because ${dependencyReport.reason}`,
677
698
  )
678
- dependencySet.add(relativeUrl)
699
+ markAsDependencyOf(relativeUrl, rootRelativeUrl)
679
700
  },
680
701
  )
681
- const removeMainRemovedCallback = projectFileRemoved.add((relativeUrl) => {
682
- if (relativeUrl === mainRelativeUrl) {
683
- removeDependencyRequestedCallback()
684
- removeMainRemovedCallback()
685
- trackerMap.delete(mainRelativeUrl)
702
+ addStopTrackingCalback(rootRelativeUrl, removeDependencyRequestedCallback)
703
+ const removeRootRemovedCallback = projectFileRemoved.add((relativeUrl) => {
704
+ if (relativeUrl === rootRelativeUrl) {
705
+ stopTrackingRoot(rootRelativeUrl)
706
+ livereloadLogger.debug(`${rootRelativeUrl} removed -> stop tracking it`)
686
707
  }
687
708
  })
709
+ addStopTrackingCalback(rootRelativeUrl, removeRootRemovedCallback)
688
710
  })
689
711
 
690
712
  const trackMainAndDependencies = (
@@ -694,20 +716,17 @@ const setupServerSentEventsForLivereload = ({
694
716
  livereloadLogger.debug(`track ${mainRelativeUrl} and its dependencies`)
695
717
 
696
718
  const removeModifiedCallback = projectFileModified.add((relativeUrl) => {
697
- const dependencySet = getDependencySet(mainRelativeUrl)
698
- if (dependencySet.has(relativeUrl)) {
719
+ if (isDependencyOf(relativeUrl, mainRelativeUrl)) {
699
720
  modified(relativeUrl)
700
721
  }
701
722
  })
702
723
  const removeRemovedCallback = projectFileRemoved.add((relativeUrl) => {
703
- const dependencySet = getDependencySet(mainRelativeUrl)
704
- if (dependencySet.has(relativeUrl)) {
724
+ if (isDependencyOf(relativeUrl, mainRelativeUrl)) {
705
725
  removed(relativeUrl)
706
726
  }
707
727
  })
708
728
  const removeAddedCallback = projectFileAdded.add((relativeUrl) => {
709
- const dependencySet = getDependencySet(mainRelativeUrl)
710
- if (dependencySet.has(relativeUrl)) {
729
+ if (isDependencyOf(relativeUrl, mainRelativeUrl)) {
711
730
  added(relativeUrl)
712
731
  }
713
732
  })
@@ -746,14 +765,6 @@ const setupServerSentEventsForLivereload = ({
746
765
 
747
766
  const { referer } = request.headers
748
767
  if (referer) {
749
- const { origin } = request
750
- // referer is likely the exploringServer
751
- if (referer !== origin && !urlIsInsideOf(referer, origin)) {
752
- return {
753
- dependency: false,
754
- reason: "referer is an other origin",
755
- }
756
- }
757
768
  // here we know the referer is inside compileServer
758
769
  const refererRelativeUrl = urlToOriginalRelativeUrl(
759
770
  referer,
@@ -766,7 +777,7 @@ const setupServerSentEventsForLivereload = ({
766
777
  for (const tracker of trackerMap) {
767
778
  if (
768
779
  tracker[0] === mainRelativeUrl &&
769
- tracker[1].has(refererRelativeUrl)
780
+ tracker[1].set.has(refererRelativeUrl)
770
781
  ) {
771
782
  return {
772
783
  dependency: true,
@@ -895,8 +906,7 @@ const createSourceFileService = ({
895
906
  projectFileCacheStrategy,
896
907
  }) => {
897
908
  return async (request) => {
898
- const { ressource } = request
899
- const relativeUrl = ressource.slice(1)
909
+ const relativeUrl = request.pathname.slice(1)
900
910
  projectFileRequestedCallback(relativeUrl, request)
901
911
 
902
912
  const responsePromise = fetchFileSystem(
@@ -38,11 +38,11 @@ export const executePlan = async (
38
38
  coverageV8ConflictWarning,
39
39
  coverageTempDirectoryRelativeUrl,
40
40
 
41
- compileServerProtocol,
42
- compileServerPrivateKey,
43
- compileServerCertificate,
44
- compileServerIp,
45
- compileServerPort,
41
+ protocol,
42
+ privateKey,
43
+ certificate,
44
+ ip,
45
+ port,
46
46
  compileServerCanReadFromFilesystem,
47
47
  compileServerCanWriteOnFilesystem,
48
48
  babelPluginMap,
@@ -99,7 +99,7 @@ export const executePlan = async (
99
99
  try {
100
100
  const compileServer = await startCompileServer({
101
101
  signal: multipleExecutionsOperation.signal,
102
- compileServerLogLevel,
102
+ logLevel: compileServerLogLevel,
103
103
 
104
104
  projectDirectoryUrl,
105
105
  jsenvDirectoryRelativeUrl,
@@ -109,11 +109,11 @@ export const executePlan = async (
109
109
  importResolutionMethod,
110
110
  importDefaultExtension,
111
111
 
112
- compileServerProtocol,
113
- compileServerPrivateKey,
114
- compileServerCertificate,
115
- compileServerIp,
116
- compileServerPort,
112
+ protocol,
113
+ privateKey,
114
+ certificate,
115
+ ip,
116
+ port,
117
117
  compileServerCanReadFromFilesystem,
118
118
  compileServerCanWriteOnFilesystem,
119
119
  keepProcessAlive: true, // to be sure it stays alive
@@ -1,22 +1,28 @@
1
1
  import { scanBrowserRuntimeFeatures } from "../runtime/createBrowserRuntime/scanBrowserRuntimeFeatures.js"
2
- import { fetchExploringJson } from "./fetchExploringJson.js"
3
2
 
4
3
  const redirect = async () => {
5
- const [browserRuntimeFeaturesReport, { exploringHtmlFileRelativeUrl }] =
6
- await Promise.all([
7
- scanBrowserRuntimeFeatures({
8
- failFastOnFeatureDetection: true,
9
- }),
10
- fetchExploringJson(),
11
- ])
4
+ const redirectTarget = new URLSearchParams(window.location.search).get(
5
+ "redirect",
6
+ )
7
+ const browserRuntimeFeaturesReport = await scanBrowserRuntimeFeatures({
8
+ failFastOnFeatureDetection: true,
9
+ })
12
10
 
13
- if (browserRuntimeFeaturesReport.canAvoidCompilation) {
14
- window.location.href = `/${exploringHtmlFileRelativeUrl}`
15
- return
16
- }
11
+ const href = `${getDirectoryUrl(
12
+ browserRuntimeFeaturesReport,
13
+ )}${redirectTarget}`
14
+ window.location.href = href
15
+ }
17
16
 
18
- const { outDirectoryRelativeUrl, compileId } = browserRuntimeFeaturesReport
19
- window.location.href = `/${outDirectoryRelativeUrl}${compileId}/${exploringHtmlFileRelativeUrl}`
17
+ const getDirectoryUrl = ({
18
+ canAvoidCompilation,
19
+ outDirectoryRelativeUrl,
20
+ compileId,
21
+ }) => {
22
+ if (canAvoidCompilation) {
23
+ return `/`
24
+ }
25
+ return `/${outDirectoryRelativeUrl}${compileId}/`
20
26
  }
21
27
 
22
28
  redirect()