@jsenv/core 23.8.2 → 23.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/jsenv_browser_system.js +34 -34
  2. package/dist/jsenv_browser_system.js.map +119 -1048
  3. package/dist/jsenv_compile_proxy.js.map +62 -364
  4. package/dist/jsenv_exploring_index.js.map +32 -338
  5. package/dist/jsenv_exploring_redirector.js.map +76 -404
  6. package/dist/jsenv_toolbar.js.map +112 -830
  7. package/dist/jsenv_toolbar_injector.js.map +27 -307
  8. package/helpers/babel/.eslintrc.cjs +24 -24
  9. package/helpers/babel/AsyncGenerator/AsyncGenerator.js +81 -81
  10. package/helpers/babel/AwaitValue/AwaitValue.js +3 -3
  11. package/helpers/babel/applyDecoratorDescriptor/applyDecoratorDescriptor.js +33 -33
  12. package/helpers/babel/arrayLikeToArray/arrayLikeToArray.js +7 -7
  13. package/helpers/babel/arrayWithHoles/arrayWithHoles.js +4 -4
  14. package/helpers/babel/arrayWithoutHoles/arrayWithoutHoles.js +6 -6
  15. package/helpers/babel/assertThisInitialized/assertThisInitialized.js +7 -7
  16. package/helpers/babel/asyncGeneratorDelegate/asyncGeneratorDelegate.js +40 -40
  17. package/helpers/babel/asyncIterator/asyncIterator.js +12 -12
  18. package/helpers/babel/asyncToGenerator/asyncToGenerator.js +34 -34
  19. package/helpers/babel/awaitAsyncGenerator/awaitAsyncGenerator.js +5 -5
  20. package/helpers/babel/classApplyDescriptorDestructureSet/classApplyDescriptorDestructureSet.js +20 -20
  21. package/helpers/babel/classApplyDescriptorGet/classApplyDescriptorGet.js +6 -6
  22. package/helpers/babel/classApplyDescriptorSet/classApplyDescriptorSet.js +13 -13
  23. package/helpers/babel/classCallCheck/classCallCheck.js +5 -5
  24. package/helpers/babel/classCheckPrivateStaticAccess/classCheckPrivateStaticAccess.js +5 -5
  25. package/helpers/babel/classCheckPrivateStaticFieldDescriptor/classCheckPrivateStaticFieldDescriptor.js +6 -6
  26. package/helpers/babel/classExtractFieldDescriptor/classExtractFieldDescriptor.js +7 -7
  27. package/helpers/babel/classNameTDZError/classNameTDZError.js +4 -4
  28. package/helpers/babel/classPrivateFieldDestructureSet/classPrivateFieldDestructureSet.js +7 -7
  29. package/helpers/babel/classPrivateFieldGet/classPrivateFieldGet.js +7 -7
  30. package/helpers/babel/classPrivateFieldLooseBase/classPrivateFieldLooseBase.js +6 -6
  31. package/helpers/babel/classPrivateFieldLooseKey/classPrivateFieldLooseKey.js +5 -5
  32. package/helpers/babel/classPrivateFieldSet/classPrivateFieldSet.js +8 -8
  33. package/helpers/babel/classPrivateMethodGet/classPrivateMethodGet.js +6 -6
  34. package/helpers/babel/classPrivateMethodSet/classPrivateMethodSet.js +3 -3
  35. package/helpers/babel/classStaticPrivateFieldSpecGet/classStaticPrivateFieldSpecGet.js +9 -9
  36. package/helpers/babel/classStaticPrivateFieldSpecSet/classStaticPrivateFieldSpecSet.js +15 -15
  37. package/helpers/babel/classStaticPrivateMethodGet/classStaticPrivateMethodGet.js +6 -6
  38. package/helpers/babel/classStaticPrivateMethodSet/classStaticPrivateMethodSet.js +3 -3
  39. package/helpers/babel/construct/construct.js +16 -16
  40. package/helpers/babel/createClass/createClass.js +15 -15
  41. package/helpers/babel/createForOfIteratorHelper/createForOfIteratorHelper.js +60 -60
  42. package/helpers/babel/createForOfIteratorHelperLoose/createForOfIteratorHelperLoose.js +23 -23
  43. package/helpers/babel/createRawReactElement/createRawReactElement.js +50 -50
  44. package/helpers/babel/createSuper/createSuper.js +22 -22
  45. package/helpers/babel/decorate/decorate.js +403 -403
  46. package/helpers/babel/defaults/defaults.js +11 -11
  47. package/helpers/babel/defineEnumerableProperties/defineEnumerableProperties.js +23 -23
  48. package/helpers/babel/defineProperty/defineProperty.js +18 -18
  49. package/helpers/babel/extends/extends.js +14 -14
  50. package/helpers/babel/get/get.js +13 -13
  51. package/helpers/babel/getPrototypeOf/getPrototypeOf.js +4 -4
  52. package/helpers/babel/inherits/inherits.js +15 -15
  53. package/helpers/babel/inheritsLoose/inheritsLoose.js +7 -7
  54. package/helpers/babel/initializerDefineProperty/initializerDefineProperty.js +10 -10
  55. package/helpers/babel/initializerWarningHelper/initializerWarningHelper.js +6 -6
  56. package/helpers/babel/instanceof/instanceof.js +6 -6
  57. package/helpers/babel/interopRequireDefault/interopRequireDefault.js +3 -3
  58. package/helpers/babel/interopRequireWildcard/interopRequireWildcard.js +37 -37
  59. package/helpers/babel/isNativeFunction/isNativeFunction.js +4 -4
  60. package/helpers/babel/isNativeReflectConstruct/isNativeReflectConstruct.js +21 -21
  61. package/helpers/babel/iterableToArray/iterableToArray.js +7 -7
  62. package/helpers/babel/iterableToArrayLimit/iterableToArrayLimit.js +36 -36
  63. package/helpers/babel/iterableToArrayLimitLoose/iterableToArrayLimitLoose.js +10 -10
  64. package/helpers/babel/jsx/jsx.js +45 -45
  65. package/helpers/babel/maybeArrayLike/maybeArrayLike.js +10 -10
  66. package/helpers/babel/newArrowCheck/newArrowCheck.js +5 -5
  67. package/helpers/babel/nonIterableRest/nonIterableRest.js +5 -5
  68. package/helpers/babel/nonIterableSpread/nonIterableSpread.js +5 -5
  69. package/helpers/babel/objectDestructuringEmpty/objectDestructuringEmpty.js +3 -3
  70. package/helpers/babel/objectSpread/objectSpread.js +23 -23
  71. package/helpers/babel/objectSpread2/objectSpread2.js +33 -33
  72. package/helpers/babel/objectWithoutProperties/objectWithoutProperties.js +19 -19
  73. package/helpers/babel/objectWithoutPropertiesLoose/objectWithoutPropertiesLoose.js +13 -13
  74. package/helpers/babel/possibleConstructorReturn/possibleConstructorReturn.js +10 -10
  75. package/helpers/babel/readOnlyError/readOnlyError.js +4 -4
  76. package/helpers/babel/readme.md +9 -9
  77. package/helpers/babel/set/set.js +44 -44
  78. package/helpers/babel/setPrototypeOf/setPrototypeOf.js +6 -6
  79. package/helpers/babel/skipFirstGeneratorNext/skipFirstGeneratorNext.js +8 -8
  80. package/helpers/babel/slicedToArray/slicedToArray.js +10 -10
  81. package/helpers/babel/slicedToArrayLoose/slicedToArrayLoose.js +13 -13
  82. package/helpers/babel/superPropBase/superPropBase.js +10 -10
  83. package/helpers/babel/taggedTemplateLiteral/taggedTemplateLiteral.js +10 -10
  84. package/helpers/babel/taggedTemplateLiteralLoose/taggedTemplateLiteralLoose.js +7 -7
  85. package/helpers/babel/tdz/tdz.js +4 -4
  86. package/helpers/babel/temporalRef/temporalRef.js +6 -6
  87. package/helpers/babel/temporalUndefined/temporalUndefined.js +3 -3
  88. package/helpers/babel/toArray/toArray.js +10 -10
  89. package/helpers/babel/toConsumableArray/toConsumableArray.js +10 -10
  90. package/helpers/babel/toPrimitive/toPrimitive.js +10 -10
  91. package/helpers/babel/toPropertyKey/toPropertyKey.js +6 -6
  92. package/helpers/babel/typeof/typeof.js +14 -14
  93. package/helpers/babel/unsupportedIterableToArray/unsupportedIterableToArray.js +12 -12
  94. package/helpers/babel/wrapAsyncGenerator/wrapAsyncGenerator.js +8 -8
  95. package/helpers/babel/wrapNativeSuper/wrapNativeSuper.js +30 -30
  96. package/helpers/babel/wrapRegExp/wrapRegExp.js +63 -63
  97. package/helpers/babel/writeOnlyError/writeOnlyError.js +4 -4
  98. package/helpers/regenerator-runtime/regenerator-runtime.js +748 -748
  99. package/{LICENSE → license} +21 -21
  100. package/package.json +1 -1
  101. package/src/buildProject.js +300 -300
  102. package/src/execute.js +184 -184
  103. package/src/internal/browser-launcher/jsenv-browser-system.js +199 -199
  104. package/src/internal/compiling/babel_plugin_import_assertions.js +121 -121
  105. package/src/internal/compiling/babel_plugin_import_metadata.js +22 -22
  106. package/src/internal/compiling/babel_plugin_import_visitor.js +84 -84
  107. package/src/internal/compiling/compile-directory/compile-asset.js +0 -7
  108. package/src/internal/compiling/compile-directory/getOrGenerateCompiledFile.js +268 -268
  109. package/src/internal/compiling/compile-directory/updateMeta.js +154 -154
  110. package/src/internal/compiling/compile-directory/validateCache.js +265 -265
  111. package/src/internal/compiling/compileFile.js +222 -224
  112. package/src/internal/compiling/compileHtml.js +550 -550
  113. package/src/internal/compiling/createCompiledFileService.js +291 -291
  114. package/src/internal/compiling/html_source_file_service.js +404 -404
  115. package/src/internal/compiling/js-compilation-service/jsenvTransform.js +270 -270
  116. package/src/internal/compiling/jsenvCompilerForHtml.js +308 -308
  117. package/src/internal/compiling/jsenvCompilerForJavaScript.js +2 -0
  118. package/src/internal/compiling/startCompileServer.js +1052 -1048
  119. package/src/internal/compiling/transformResultToCompilationResult.js +220 -220
  120. package/src/internal/executing/coverage/babel_plugin_instrument.js +90 -90
  121. package/src/internal/executing/coverage/reportToCoverage.js +187 -187
  122. package/src/internal/executing/executePlan.js +183 -183
  123. package/src/internal/executing/launchAndExecute.js +458 -458
  124. package/src/internal/runtime/createBrowserRuntime/scanBrowserRuntimeFeatures.js +246 -246
  125. package/src/internal/runtime/createNodeRuntime/scanNodeRuntimeFeatures.js +112 -112
  126. package/src/internal/runtime/s.js +727 -727
  127. package/src/internal/toolbar/jsenv-logo.svg +144 -144
  128. package/src/internal/toolbar/toolbar.main.css +196 -196
  129. package/src/internal/toolbar/toolbar.main.js +227 -227
  130. package/src/internal/url_conversion.js +317 -317
  131. package/src/startExploring.js +309 -309
@@ -1,1048 +1,1052 @@
1
- import { readFileSync } from "node:fs"
2
- import {
3
- jsenvAccessControlAllowedHeaders,
4
- startServer,
5
- fetchFileSystem,
6
- createSSERoom,
7
- composeServicesWithTiming,
8
- urlToContentType,
9
- pluginServerTiming,
10
- pluginRequestWaitingCheck,
11
- pluginCORS,
12
- } from "@jsenv/server"
13
- import { createLogger, createDetailedMessage } from "@jsenv/logger"
14
- import {
15
- resolveUrl,
16
- urlToFileSystemPath,
17
- urlToRelativeUrl,
18
- resolveDirectoryUrl,
19
- readFile,
20
- writeFile,
21
- ensureEmptyDirectory,
22
- registerDirectoryLifecycle,
23
- urlIsInsideOf,
24
- urlToBasename,
25
- } from "@jsenv/filesystem"
26
- import {
27
- createCallbackList,
28
- createCallbackListNotifiedOnce,
29
- } from "@jsenv/abort"
30
-
31
- import { isBrowserPartOfSupportedRuntimes } from "@jsenv/core/src/internal/generateGroupMap/runtime_support.js"
32
- import { loadBabelPluginMapFromFile } from "./load_babel_plugin_map_from_file.js"
33
- import { extractSyntaxBabelPluginMap } from "./babel_plugins.js"
34
- import { generateGroupMap } from "../generateGroupMap/generateGroupMap.js"
35
- import {
36
- jsenvCompileProxyFileInfo,
37
- sourcemapMainFileInfo,
38
- sourcemapMappingFileInfo,
39
- } from "../jsenvInternalFiles.js"
40
- import { jsenvCoreDirectoryUrl } from "../jsenvCoreDirectoryUrl.js"
41
- import { babelPluginReplaceExpressions } from "../babel_plugin_replace_expressions.js"
42
- import { babelPluginGlobalThisAsJsenvImport } from "./babel_plugin_global_this_as_jsenv_import.js"
43
- import { babelPluginNewStylesheetAsJsenvImport } from "./babel_plugin_new_stylesheet_as_jsenv_import.js"
44
- import { babelPluginImportAssertions } from "./babel_plugin_import_assertions.js"
45
- import { createCompiledFileService } from "./createCompiledFileService.js"
46
- import { urlIsCompilationAsset } from "./compile-directory/compile-asset.js"
47
- import { createTransformHtmlSourceFileService } from "./html_source_file_service.js"
48
-
49
- export const startCompileServer = async ({
50
- signal = new AbortController().signal,
51
- handleSIGINT,
52
- compileServerLogLevel,
53
-
54
- projectDirectoryUrl,
55
-
56
- importDefaultExtension,
57
-
58
- jsenvDirectoryRelativeUrl = ".jsenv",
59
- jsenvDirectoryClean = false,
60
- outDirectoryName = "out",
61
-
62
- sourcemapMethod = "comment", // "inline" is also possible
63
- sourcemapExcludeSources = false, // this should increase perf (no need to download source for browser)
64
- compileServerCanReadFromFilesystem = true,
65
- compileServerCanWriteOnFilesystem = true,
66
- compileCacheStrategy = "mtime",
67
- projectFileCacheStrategy = "mtime",
68
-
69
- // js compile options
70
- transformTopLevelAwait = true,
71
- moduleOutFormat = "systemjs",
72
- importMetaFormat = moduleOutFormat,
73
- env = {},
74
- processEnvNodeEnv = process.env.NODE_ENV,
75
- replaceProcessEnvNodeEnv = true,
76
- replaceGlobalObject = false,
77
- replaceGlobalFilename = false,
78
- replaceGlobalDirname = false,
79
- replaceMap = {},
80
- babelPluginMap,
81
- babelConfigFileUrl,
82
- customCompilers = {},
83
-
84
- // options related to the server itself
85
- compileServerProtocol = "http",
86
- compileServerHttp2 = compileServerProtocol === "https",
87
- compileServerPrivateKey,
88
- compileServerCertificate,
89
- compileServerIp = "0.0.0.0",
90
- compileServerPort = 0,
91
- keepProcessAlive = false,
92
- onStop = () => {},
93
-
94
- // remaining options
95
- runtimeSupport,
96
-
97
- livereloadWatchConfig = {
98
- "./**": true,
99
- "./**/.*/": false, // any folder starting with a dot is ignored (includes .git for instance)
100
- "./dist/": false,
101
- "./**/node_modules/": false,
102
- },
103
- livereloadLogLevel = "info",
104
- customServices = {},
105
- livereloadSSE = false,
106
- transformHtmlSourceFiles = true,
107
- jsenvToolbarInjection = false,
108
- jsenvScriptInjection = true,
109
- inlineImportMapIntoHTML = true,
110
- }) => {
111
- assertArguments({
112
- projectDirectoryUrl,
113
- jsenvDirectoryRelativeUrl,
114
- outDirectoryName,
115
- })
116
-
117
- const jsenvDirectoryUrl = resolveDirectoryUrl(
118
- jsenvDirectoryRelativeUrl,
119
- projectDirectoryUrl,
120
- )
121
- const outDirectoryUrl = resolveDirectoryUrl(
122
- outDirectoryName,
123
- jsenvDirectoryUrl,
124
- )
125
- const outDirectoryRelativeUrl = urlToRelativeUrl(
126
- outDirectoryUrl,
127
- projectDirectoryUrl,
128
- )
129
- // normalization
130
- jsenvDirectoryRelativeUrl = urlToRelativeUrl(
131
- jsenvDirectoryUrl,
132
- projectDirectoryUrl,
133
- )
134
-
135
- const logger = createLogger({ logLevel: compileServerLogLevel })
136
-
137
- const browser = isBrowserPartOfSupportedRuntimes(runtimeSupport)
138
- const babelPluginMapFromFile = await loadBabelPluginMapFromFile({
139
- projectDirectoryUrl,
140
- babelConfigFileUrl,
141
- })
142
- babelPluginMap = {
143
- "global-this-as-jsenv-import": babelPluginGlobalThisAsJsenvImport,
144
- "new-stylesheet-as-jsenv-import": babelPluginNewStylesheetAsJsenvImport,
145
- "transform-import-assertions": babelPluginImportAssertions,
146
- ...babelPluginMapFromFile,
147
- ...babelPluginMap,
148
- }
149
- Object.keys(babelPluginMap).forEach((key) => {
150
- if (
151
- key === "transform-modules-commonjs" ||
152
- key === "transform-modules-amd" ||
153
- key === "transform-modules-systemjs"
154
- ) {
155
- const declaredInFile = Boolean(babelPluginMapFromFile[key])
156
- logger.warn(
157
- createDetailedMessage(
158
- `WARNING: "${key}" babel plugin should not be enabled, it will be ignored`,
159
- {
160
- suggestion: declaredInFile
161
- ? `To get rid of this warning, remove "${key}" from babel config file. Either with "modules": false in @babel/preset-env or removing "@babel/${key}" from plugins`
162
- : `To get rid of this warning, remove "${key}" from babelPluginMap parameter`,
163
- },
164
- ),
165
- )
166
- delete babelPluginMap[key]
167
- }
168
- })
169
-
170
- const { babelSyntaxPluginMap, babelPluginMapWithoutSyntax } =
171
- extractSyntaxBabelPluginMap(babelPluginMap)
172
- const compileServerGroupMap = generateGroupMap({
173
- babelPluginMap: babelPluginMapWithoutSyntax,
174
- runtimeSupport,
175
- })
176
-
177
- babelPluginMap = {
178
- // When code should be compatible with browsers, ensure
179
- // process.env.NODE_ENV is replaced to be executable in a browser by forcing
180
- // "transform-replace-expressions" babel plugin.
181
- // It happens for module written in ESM but also using process.env.NODE_ENV
182
- // for example "react-redux"
183
- // This babel plugin won't force compilation because it's added after "generateGroupMap"
184
- // however it will be used even if not part of "pluginRequiredNameArray"
185
- // as visible in "babelPluginMapFromCompileId"
186
- // This is a quick workaround to get things working because:
187
- // - If none of your code needs to be compiled but one of your dependency
188
- // uses process.env.NODE_ENV, the code will throw "process" is undefined
189
- // This is fine but you won't have a dedicated way to force compilation to ensure
190
- // "process.env.NODE_ENV" is replaced.
191
- // Ideally this should be a custom compiler dedicated for this use case. It's not the case
192
- // for now because it was faster to do it this way and the use case is a bit blurry:
193
- // What should this custom compiler do? Just replace some node globals? How would it be named and documented?
194
- ...(browser
195
- ? {
196
- "transform-replace-expressions": [
197
- babelPluginReplaceExpressions,
198
- {
199
- replaceMap: {
200
- ...(replaceProcessEnvNodeEnv
201
- ? { "process.env.NODE_ENV": `("${processEnvNodeEnv}")` }
202
- : {}),
203
- ...(replaceGlobalObject ? { global: "globalThis" } : {}),
204
- ...(replaceGlobalFilename
205
- ? { __filename: __filenameReplacement }
206
- : {}),
207
- ...(replaceGlobalDirname
208
- ? { __dirname: __dirnameReplacement }
209
- : {}),
210
- ...replaceMap,
211
- },
212
- allowConflictingReplacements: true,
213
- },
214
- ],
215
- }
216
- : {}),
217
- ...babelSyntaxPluginMap,
218
- ...babelPluginMap,
219
- }
220
-
221
- const serverStopCallbackList = createCallbackListNotifiedOnce()
222
-
223
- let projectFileRequestedCallback = () => {}
224
- if (livereloadSSE) {
225
- const sseSetup = setupServerSentEventsForLivereload({
226
- serverStopCallbackList,
227
- projectDirectoryUrl,
228
- jsenvDirectoryRelativeUrl,
229
- outDirectoryRelativeUrl,
230
- livereloadLogLevel,
231
- livereloadWatchConfig,
232
- })
233
-
234
- projectFileRequestedCallback = sseSetup.projectFileRequestedCallback
235
- const serveSSEForLivereload = createSSEForLivereloadService({
236
- serverStopCallbackList,
237
- outDirectoryRelativeUrl,
238
- trackMainAndDependencies: sseSetup.trackMainAndDependencies,
239
- })
240
- customServices = {
241
- "service:sse": serveSSEForLivereload,
242
- ...customServices,
243
- }
244
- } else {
245
- const roomWhenLivereloadIsDisabled = createSSERoom()
246
- roomWhenLivereloadIsDisabled.open()
247
- customServices = {
248
- "service:sse": (request) => {
249
- const { accept } = request.headers
250
- if (!accept || !accept.includes("text/event-stream")) {
251
- return null
252
- }
253
- return roomWhenLivereloadIsDisabled.join(request)
254
- },
255
- ...customServices,
256
- }
257
- }
258
-
259
- const compileServerMetaFileInfo = createCompileServerMetaFileInfo({
260
- projectDirectoryUrl,
261
- jsenvDirectoryRelativeUrl,
262
- outDirectoryRelativeUrl,
263
- importDefaultExtension,
264
- compileServerGroupMap,
265
- env,
266
- inlineImportMapIntoHTML,
267
- babelPluginMap,
268
- customCompilers,
269
- jsenvToolbarInjection,
270
- sourcemapMethod,
271
- sourcemapExcludeSources,
272
- })
273
- if (compileServerCanWriteOnFilesystem) {
274
- await cleanOutDirectoryIfNeeded({
275
- logger,
276
- outDirectoryUrl,
277
- jsenvDirectoryUrl,
278
- jsenvDirectoryClean,
279
- compileServerMetaFileInfo,
280
- })
281
- writeFile(
282
- compileServerMetaFileInfo.url,
283
- JSON.stringify(compileServerMetaFileInfo.data, null, " "),
284
- )
285
- logger.debug(`-> ${compileServerMetaFileInfo.url}`)
286
- }
287
-
288
- const jsenvServices = {
289
- "service:compilation asset": createCompilationAssetFileService({
290
- projectDirectoryUrl,
291
- }),
292
- "service:compile server meta": createCompileServerMetaService({
293
- projectDirectoryUrl,
294
- outDirectoryUrl,
295
- compileServerMetaFileInfo,
296
- }),
297
- "service: compile proxy": createCompileProxyService({
298
- projectDirectoryUrl,
299
- }),
300
- "service:compiled file": createCompiledFileService({
301
- logger,
302
-
303
- projectDirectoryUrl,
304
- outDirectoryRelativeUrl,
305
-
306
- importDefaultExtension,
307
-
308
- runtimeSupport,
309
- transformTopLevelAwait,
310
- groupMap: compileServerGroupMap,
311
- babelPluginMap,
312
- customCompilers,
313
- moduleOutFormat,
314
- importMetaFormat,
315
- jsenvToolbarInjection,
316
-
317
- projectFileRequestedCallback,
318
- sourcemapMethod,
319
- sourcemapExcludeSources,
320
- compileCacheStrategy: compileServerCanReadFromFilesystem
321
- ? compileCacheStrategy
322
- : "none",
323
- }),
324
- ...(transformHtmlSourceFiles
325
- ? {
326
- "service:transform html source file":
327
- createTransformHtmlSourceFileService({
328
- logger,
329
- projectDirectoryUrl,
330
- inlineImportMapIntoHTML,
331
- jsenvScriptInjection,
332
- jsenvToolbarInjection,
333
- }),
334
- }
335
- : {}),
336
- "service:source file": createSourceFileService({
337
- projectDirectoryUrl,
338
- projectFileRequestedCallback,
339
- projectFileCacheStrategy,
340
- }),
341
- }
342
-
343
- const compileServer = await startServer({
344
- signal,
345
- stopOnExit: false,
346
- stopOnSIGINT: handleSIGINT,
347
- stopOnInternalError: false,
348
- sendServerInternalErrorDetails: true,
349
- keepProcessAlive,
350
-
351
- logLevel: compileServerLogLevel,
352
-
353
- protocol: compileServerProtocol,
354
- http2: compileServerHttp2,
355
- serverCertificate: compileServerCertificate,
356
- serverCertificatePrivateKey: compileServerPrivateKey,
357
- ip: compileServerIp,
358
- port: compileServerPort,
359
- plugins: {
360
- ...pluginCORS({
361
- accessControlAllowRequestOrigin: true,
362
- accessControlAllowRequestMethod: true,
363
- accessControlAllowRequestHeaders: true,
364
- accessControlAllowedRequestHeaders: [
365
- ...jsenvAccessControlAllowedHeaders,
366
- "x-jsenv-execution-id",
367
- ],
368
- accessControlAllowCredentials: true,
369
- }),
370
- ...pluginServerTiming,
371
- ...pluginRequestWaitingCheck({
372
- requestWaitingMs: 60 * 1000,
373
- }),
374
- },
375
- requestToResponse: composeServicesWithTiming({
376
- ...customServices,
377
- ...jsenvServices,
378
- }),
379
- onStop: (reason) => {
380
- onStop()
381
- serverStopCallbackList.notify(reason)
382
- },
383
- })
384
-
385
- return {
386
- jsenvDirectoryRelativeUrl,
387
- outDirectoryRelativeUrl,
388
- ...compileServer,
389
- compileServerGroupMap,
390
- babelPluginMap,
391
- }
392
- }
393
-
394
- export const computeOutDirectoryRelativeUrl = ({
395
- projectDirectoryUrl,
396
- jsenvDirectoryRelativeUrl = ".jsenv",
397
- outDirectoryName = "out",
398
- }) => {
399
- const jsenvDirectoryUrl = resolveDirectoryUrl(
400
- jsenvDirectoryRelativeUrl,
401
- projectDirectoryUrl,
402
- )
403
- const outDirectoryUrl = resolveDirectoryUrl(
404
- outDirectoryName,
405
- jsenvDirectoryUrl,
406
- )
407
- const outDirectoryRelativeUrl = urlToRelativeUrl(
408
- outDirectoryUrl,
409
- projectDirectoryUrl,
410
- )
411
-
412
- return outDirectoryRelativeUrl
413
- }
414
-
415
- const assertArguments = ({
416
- projectDirectoryUrl,
417
- jsenvDirectoryRelativeUrl,
418
- outDirectoryName,
419
- }) => {
420
- if (typeof projectDirectoryUrl !== "string") {
421
- throw new TypeError(
422
- `projectDirectoryUrl must be a string. got ${projectDirectoryUrl}`,
423
- )
424
- }
425
-
426
- if (typeof jsenvDirectoryRelativeUrl !== "string") {
427
- throw new TypeError(
428
- `jsenvDirectoryRelativeUrl must be a string. got ${jsenvDirectoryRelativeUrl}`,
429
- )
430
- }
431
- const jsenvDirectoryUrl = resolveDirectoryUrl(
432
- jsenvDirectoryRelativeUrl,
433
- projectDirectoryUrl,
434
- )
435
-
436
- if (!jsenvDirectoryUrl.startsWith(projectDirectoryUrl)) {
437
- throw new TypeError(
438
- createDetailedMessage(
439
- `jsenv directory must be inside project directory`,
440
- {
441
- ["jsenv directory url"]: jsenvDirectoryUrl,
442
- ["project directory url"]: projectDirectoryUrl,
443
- },
444
- ),
445
- )
446
- }
447
-
448
- if (typeof outDirectoryName !== "string") {
449
- throw new TypeError(
450
- `outDirectoryName must be a string. got ${outDirectoryName}`,
451
- )
452
- }
453
- }
454
-
455
- const cleanOutDirectoryIfNeeded = async ({
456
- logger,
457
- outDirectoryUrl,
458
- jsenvDirectoryClean,
459
- jsenvDirectoryUrl,
460
- compileServerMetaFileInfo,
461
- }) => {
462
- if (jsenvDirectoryClean) {
463
- logger.debug(
464
- `Cleaning jsenv directory because jsenvDirectoryClean parameter enabled`,
465
- )
466
- await ensureEmptyDirectory(jsenvDirectoryUrl)
467
- }
468
-
469
- let previousCompileServerMeta
470
- try {
471
- const source = await readFile(compileServerMetaFileInfo.url)
472
- previousCompileServerMeta = JSON.parse(source)
473
- } catch (e) {
474
- if (e.code === "ENOENT") {
475
- previousCompileServerMeta = null
476
- } else {
477
- throw e
478
- }
479
- }
480
-
481
- if (previousCompileServerMeta !== null) {
482
- const outDirectoryChanges = getOutDirectoryChanges(
483
- previousCompileServerMeta,
484
- compileServerMetaFileInfo.data,
485
- )
486
-
487
- if (outDirectoryChanges) {
488
- if (!jsenvDirectoryClean) {
489
- logger.debug(
490
- createDetailedMessage(
491
- `Cleaning jsenv ${urlToBasename(
492
- outDirectoryUrl.slice(0, -1),
493
- )} directory because configuration has changed.`,
494
- {
495
- "changes": outDirectoryChanges.namedChanges
496
- ? outDirectoryChanges.namedChanges
497
- : `something`,
498
- "out directory": urlToFileSystemPath(outDirectoryUrl),
499
- },
500
- ),
501
- )
502
- }
503
- await ensureEmptyDirectory(outDirectoryUrl)
504
- }
505
- }
506
- }
507
-
508
- const getOutDirectoryChanges = (
509
- previousCompileServerMeta,
510
- compileServerMeta,
511
- ) => {
512
- const changes = []
513
-
514
- Object.keys(compileServerMeta).forEach((key) => {
515
- const now = compileServerMeta[key]
516
- const previous = previousCompileServerMeta[key]
517
- if (!compareValueJson(now, previous)) {
518
- changes.push(key)
519
- }
520
- })
521
-
522
- if (changes.length > 0) {
523
- return { namedChanges: changes }
524
- }
525
-
526
- // in case basic comparison from above is not enough
527
- if (!compareValueJson(previousCompileServerMeta, compileServerMeta)) {
528
- return { somethingChanged: true }
529
- }
530
-
531
- return null
532
- }
533
-
534
- const compareValueJson = (left, right) => {
535
- return JSON.stringify(left) === JSON.stringify(right)
536
- }
537
-
538
- /**
539
- * We need to get two things:
540
- * { projectFileRequestedCallback, trackMainAndDependencies }
541
- *
542
- * projectFileRequestedCallback
543
- * This function will be called by the compile server every time a file inside projectDirectory
544
- * is requested so that we can build up the dependency tree of any file
545
- *
546
- * trackMainAndDependencies
547
- * This function is meant to be used to implement server sent events in order for a client to know
548
- * when a given file or any of its dependencies changes in order to implement livereloading.
549
- * At any time this function can be called with (mainRelativeUrl, { modified, removed, lastEventId })
550
- * modified is called
551
- * - immediatly if lastEventId is passed and mainRelativeUrl or any of its dependencies have
552
- * changed since that event (last change is passed to modified if their is more than one change)
553
- * - when mainRelativeUrl or any of its dependencies is modified
554
- * removed is called
555
- * - with same spec as modified but when a file is deleted from the filesystem instead of modified
556
- *
557
- */
558
- const setupServerSentEventsForLivereload = ({
559
- serverStopCallbackList,
560
- projectDirectoryUrl,
561
- jsenvDirectoryRelativeUrl,
562
- outDirectoryRelativeUrl,
563
- livereloadLogLevel,
564
- livereloadWatchConfig,
565
- }) => {
566
- const livereloadLogger = createLogger({ logLevel: livereloadLogLevel })
567
- const trackerMap = new Map()
568
- const projectFileRequested = createCallbackList()
569
- const projectFileModified = createCallbackList()
570
- const projectFileRemoved = createCallbackList()
571
- const projectFileAdded = createCallbackList()
572
-
573
- const projectFileRequestedCallback = (relativeUrl, request) => {
574
- // I doubt a compilation asset like .js.map will change
575
- // in theory a compilation asset should not change
576
- // if the source file did not change
577
- // so we can avoid watching compilation asset
578
- if (urlIsCompilationAsset(`${projectDirectoryUrl}${relativeUrl}`)) {
579
- return
580
- }
581
-
582
- projectFileRequested.notify({ relativeUrl, request })
583
- }
584
- const watchDescription = {
585
- ...livereloadWatchConfig,
586
- [jsenvDirectoryRelativeUrl]: false,
587
- }
588
- const unregisterDirectoryLifecyle = registerDirectoryLifecycle(
589
- projectDirectoryUrl,
590
- {
591
- watchDescription,
592
- updated: ({ relativeUrl }) => {
593
- projectFileModified.notify(relativeUrl)
594
- },
595
- removed: ({ relativeUrl }) => {
596
- projectFileRemoved.notify(relativeUrl)
597
- },
598
- added: ({ relativeUrl }) => {
599
- projectFileAdded.notify(relativeUrl)
600
- },
601
- keepProcessAlive: false,
602
- recursive: true,
603
- },
604
- )
605
- serverStopCallbackList.add(unregisterDirectoryLifecyle)
606
-
607
- const getDependencySet = (mainRelativeUrl) => {
608
- if (trackerMap.has(mainRelativeUrl)) {
609
- return trackerMap.get(mainRelativeUrl)
610
- }
611
- const dependencySet = new Set()
612
- dependencySet.add(mainRelativeUrl)
613
- trackerMap.set(mainRelativeUrl, dependencySet)
614
- return dependencySet
615
- }
616
-
617
- // each time a file is requested for the first time its dependencySet is computed
618
- projectFileRequested.add(({ relativeUrl: mainRelativeUrl }) => {
619
- // for now no use case of livereloading on node.js
620
- // and for browsers only html file can be main files
621
- // this avoid collecting dependencies of non html files that will never be used
622
- if (!mainRelativeUrl.endsWith(".html")) {
623
- return
624
- }
625
-
626
- // when a file is requested, always rebuild its dependency in case it has changed
627
- // since the last time it was requested
628
- const dependencySet = new Set()
629
- dependencySet.add(mainRelativeUrl)
630
- trackerMap.set(mainRelativeUrl, dependencySet)
631
-
632
- const removeDependencyRequestedCallback = projectFileRequested.add(
633
- ({ relativeUrl, request }) => {
634
- if (dependencySet.has(relativeUrl)) {
635
- return
636
- }
637
-
638
- const dependencyReport = reportDependency(
639
- relativeUrl,
640
- mainRelativeUrl,
641
- request,
642
- )
643
- if (dependencyReport.dependency === false) {
644
- livereloadLogger.debug(
645
- `${relativeUrl} not a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
646
- )
647
- return
648
- }
649
-
650
- livereloadLogger.debug(
651
- `${relativeUrl} is a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
652
- )
653
- dependencySet.add(relativeUrl)
654
- },
655
- )
656
- const removeMainRemovedCallback = projectFileRemoved.add((relativeUrl) => {
657
- if (relativeUrl === mainRelativeUrl) {
658
- removeDependencyRequestedCallback()
659
- removeMainRemovedCallback()
660
- trackerMap.delete(mainRelativeUrl)
661
- }
662
- })
663
- })
664
-
665
- const trackMainAndDependencies = (
666
- mainRelativeUrl,
667
- { modified, removed, added },
668
- ) => {
669
- livereloadLogger.debug(`track ${mainRelativeUrl} and its dependencies`)
670
-
671
- const removeModifiedCallback = projectFileModified.add((relativeUrl) => {
672
- const dependencySet = getDependencySet(mainRelativeUrl)
673
- if (dependencySet.has(relativeUrl)) {
674
- modified(relativeUrl)
675
- }
676
- })
677
- const removeRemovedCallback = projectFileRemoved.add((relativeUrl) => {
678
- const dependencySet = getDependencySet(mainRelativeUrl)
679
- if (dependencySet.has(relativeUrl)) {
680
- removed(relativeUrl)
681
- }
682
- })
683
- const removeAddedCallback = projectFileAdded.add((relativeUrl) => {
684
- const dependencySet = getDependencySet(mainRelativeUrl)
685
- if (dependencySet.has(relativeUrl)) {
686
- added(relativeUrl)
687
- }
688
- })
689
-
690
- return () => {
691
- livereloadLogger.debug(
692
- `stop tracking ${mainRelativeUrl} and its dependencies.`,
693
- )
694
- removeModifiedCallback()
695
- removeRemovedCallback()
696
- removeAddedCallback()
697
- }
698
- }
699
-
700
- const reportDependency = (relativeUrl, mainRelativeUrl, request) => {
701
- if (relativeUrl === mainRelativeUrl) {
702
- return {
703
- dependency: true,
704
- reason: "it's main",
705
- }
706
- }
707
-
708
- if ("x-jsenv-execution-id" in request.headers) {
709
- const executionId = request.headers["x-jsenv-execution-id"]
710
- if (executionId === mainRelativeUrl) {
711
- return {
712
- dependency: true,
713
- reason: "x-jsenv-execution-id request header",
714
- }
715
- }
716
- return {
717
- dependency: false,
718
- reason: "x-jsenv-execution-id request header",
719
- }
720
- }
721
-
722
- const { referer } = request.headers
723
- if (referer) {
724
- const { origin } = request
725
- // referer is likely the exploringServer
726
- if (referer !== origin && !urlIsInsideOf(referer, origin)) {
727
- return {
728
- dependency: false,
729
- reason: "referer is an other origin",
730
- }
731
- }
732
- // here we know the referer is inside compileServer
733
- const refererRelativeUrl = urlToOriginalRelativeUrl(
734
- referer,
735
- resolveUrl(outDirectoryRelativeUrl, request.origin),
736
- )
737
- if (refererRelativeUrl) {
738
- // search if referer (file requesting this one) is tracked as being a dependency of main file
739
- // in that case because the importer is a dependency the importee is also a dependency
740
- // eslint-disable-next-line no-unused-vars
741
- for (const tracker of trackerMap) {
742
- if (
743
- tracker[0] === mainRelativeUrl &&
744
- tracker[1].has(refererRelativeUrl)
745
- ) {
746
- return {
747
- dependency: true,
748
- reason: "referer is a dependency",
749
- }
750
- }
751
- }
752
- }
753
- }
754
-
755
- return {
756
- dependency: true,
757
- reason: "it was requested",
758
- }
759
- }
760
-
761
- return {
762
- projectFileRequestedCallback,
763
- trackMainAndDependencies,
764
- }
765
- }
766
-
767
- const createSSEForLivereloadService = ({
768
- serverStopCallbackList,
769
- outDirectoryRelativeUrl,
770
- trackMainAndDependencies,
771
- }) => {
772
- const cache = []
773
- const sseRoomLimit = 100
774
- const getOrCreateSSERoom = (mainFileRelativeUrl) => {
775
- const cacheEntry = cache.find(
776
- (cacheEntryCandidate) =>
777
- cacheEntryCandidate.mainFileRelativeUrl === mainFileRelativeUrl,
778
- )
779
- if (cacheEntry) {
780
- return cacheEntry.sseRoom
781
- }
782
-
783
- const sseRoom = createSSERoom({
784
- retryDuration: 2000,
785
- historyLength: 100,
786
- welcomeEventEnabled: true,
787
- })
788
-
789
- // each time something is modified or removed we send event to the room
790
- const stopTracking = trackMainAndDependencies(mainFileRelativeUrl, {
791
- modified: (relativeUrl) => {
792
- sseRoom.sendEvent({ type: "file-modified", data: relativeUrl })
793
- },
794
- removed: (relativeUrl) => {
795
- sseRoom.sendEvent({ type: "file-removed", data: relativeUrl })
796
- },
797
- added: (relativeUrl) => {
798
- sseRoom.sendEvent({ type: "file-added", data: relativeUrl })
799
- },
800
- })
801
-
802
- const removeSSECleanupCallback = serverStopCallbackList.add(() => {
803
- removeSSECleanupCallback()
804
- sseRoom.close()
805
- stopTracking()
806
- })
807
- cache.push({
808
- mainFileRelativeUrl,
809
- sseRoom,
810
- cleanup: () => {
811
- removeSSECleanupCallback()
812
- sseRoom.close()
813
- stopTracking()
814
- },
815
- })
816
- if (cache.length >= sseRoomLimit) {
817
- const firstCacheEntry = cache.shift()
818
- firstCacheEntry.cleanup()
819
- }
820
- return sseRoom
821
- }
822
-
823
- return (request) => {
824
- const { accept } = request.headers
825
- if (!accept || !accept.includes("text/event-stream")) {
826
- return null
827
- }
828
-
829
- const fileRelativeUrl = urlToOriginalRelativeUrl(
830
- resolveUrl(request.ressource, request.origin),
831
- resolveUrl(outDirectoryRelativeUrl, request.origin),
832
- )
833
-
834
- const room = getOrCreateSSERoom(fileRelativeUrl)
835
- return room.join(request)
836
- }
837
- }
838
-
839
- const urlToOriginalRelativeUrl = (url, outDirectoryRemoteUrl) => {
840
- if (urlIsInsideOf(url, outDirectoryRemoteUrl)) {
841
- const afterCompileDirectory = urlToRelativeUrl(url, outDirectoryRemoteUrl)
842
- const fileRelativeUrl = afterCompileDirectory.slice(
843
- afterCompileDirectory.indexOf("/") + 1,
844
- )
845
- return fileRelativeUrl
846
- }
847
- return new URL(url).pathname.slice(1)
848
- }
849
-
850
- const createCompilationAssetFileService = ({ projectDirectoryUrl }) => {
851
- return (request) => {
852
- const { origin, ressource } = request
853
- const requestUrl = `${origin}${ressource}`
854
- if (urlIsCompilationAsset(requestUrl)) {
855
- return fetchFileSystem(
856
- new URL(request.ressource.slice(1), projectDirectoryUrl),
857
- {
858
- headers: request.headers,
859
- etagEnabled: true,
860
- },
861
- )
862
- }
863
- return null
864
- }
865
- }
866
-
867
- const createSourceFileService = ({
868
- projectDirectoryUrl,
869
- projectFileRequestedCallback,
870
- projectFileCacheStrategy,
871
- }) => {
872
- return async (request) => {
873
- const { ressource } = request
874
- const relativeUrl = ressource.slice(1)
875
- projectFileRequestedCallback(relativeUrl, request)
876
-
877
- const responsePromise = fetchFileSystem(
878
- new URL(request.ressource.slice(1), projectDirectoryUrl),
879
- {
880
- headers: request.headers,
881
- etagEnabled: projectFileCacheStrategy === "etag",
882
- mtimeEnabled: projectFileCacheStrategy === "mtime",
883
- },
884
- )
885
-
886
- return responsePromise
887
- }
888
- }
889
-
890
- const createCompileServerMetaFileInfo = ({
891
- projectDirectoryUrl,
892
- jsenvDirectoryRelativeUrl,
893
- outDirectoryRelativeUrl,
894
- importDefaultExtension,
895
- compileServerGroupMap,
896
- babelPluginMap,
897
- replaceProcessEnvNodeEnv,
898
- processEnvNodeEnv,
899
- env,
900
- inlineImportMapIntoHTML,
901
- customCompilers,
902
- jsenvToolbarInjection,
903
- sourcemapMethod,
904
- sourcemapExcludeSources,
905
- }) => {
906
- const outDirectoryUrl = resolveUrl(
907
- outDirectoryRelativeUrl,
908
- projectDirectoryUrl,
909
- )
910
- const compileServerMetaFileUrl = resolveUrl(
911
- "./__compile_server_meta__.json",
912
- outDirectoryUrl,
913
- )
914
- const jsenvCorePackageFileUrl = resolveUrl(
915
- "./package.json",
916
- jsenvCoreDirectoryUrl,
917
- )
918
- const jsenvCorePackageFilePath = urlToFileSystemPath(jsenvCorePackageFileUrl)
919
- const jsenvCorePackageVersion = readPackage(jsenvCorePackageFilePath).version
920
- const customCompilerPatterns = Object.keys(customCompilers)
921
- const sourcemapMainFileRelativeUrl = urlToRelativeUrl(
922
- sourcemapMainFileInfo.url,
923
- projectDirectoryUrl,
924
- )
925
- const sourcemapMappingFileRelativeUrl = urlToRelativeUrl(
926
- sourcemapMappingFileInfo.url,
927
- projectDirectoryUrl,
928
- )
929
- const compileServerMeta = {
930
- jsenvDirectoryRelativeUrl,
931
- outDirectoryRelativeUrl,
932
- importDefaultExtension,
933
-
934
- babelPluginMap: babelPluginMapAsData(babelPluginMap),
935
- compileServerGroupMap,
936
- customCompilerPatterns,
937
- replaceProcessEnvNodeEnv,
938
- processEnvNodeEnv,
939
- inlineImportMapIntoHTML,
940
-
941
- sourcemapMethod,
942
- sourcemapExcludeSources,
943
- sourcemapMainFileRelativeUrl,
944
- sourcemapMappingFileRelativeUrl,
945
- errorStackRemapping: true,
946
-
947
- jsenvCorePackageVersion,
948
- jsenvToolbarInjection,
949
- env,
950
- }
951
- return {
952
- url: compileServerMetaFileUrl,
953
- data: compileServerMeta,
954
- }
955
- }
956
-
957
- const babelPluginMapAsData = (babelPluginMap) => {
958
- const data = {}
959
- Object.keys(babelPluginMap).forEach((key) => {
960
- const value = babelPluginMap[key]
961
- if (Array.isArray(value)) {
962
- data[key] = value
963
- return
964
- }
965
- if (typeof value === "object") {
966
- data[key] = {
967
- options: value.options,
968
- }
969
- return
970
- }
971
- data[key] = value
972
- })
973
- return data
974
- }
975
-
976
- const createCompileServerMetaService = ({
977
- projectDirectoryUrl,
978
- outDirectoryUrl,
979
- compileServerMetaFileInfo,
980
- }) => {
981
- const isCompileServerMetaFile = (url) => {
982
- if (!urlIsInsideOf(url, outDirectoryUrl)) {
983
- return false
984
- }
985
- const afterOutDirectory = url.slice(outDirectoryUrl.length)
986
- if (afterOutDirectory.indexOf("/") > -1) {
987
- return false
988
- }
989
- return true
990
- }
991
-
992
- // serve from memory
993
- return (request) => {
994
- const requestUrl = resolveUrl(
995
- request.ressource.slice(1),
996
- projectDirectoryUrl,
997
- )
998
- if (
999
- isCompileServerMetaFile(requestUrl) ||
1000
- // allow to request it directly from .jsenv
1001
- request.ressource === "/.jsenv/__compile_server_meta__.json"
1002
- ) {
1003
- const body = JSON.stringify(compileServerMetaFileInfo.data, null, " ")
1004
- return {
1005
- status: 200,
1006
- headers: {
1007
- "content-type": urlToContentType(requestUrl),
1008
- "content-length": Buffer.byteLength(body),
1009
- },
1010
- body,
1011
- }
1012
- }
1013
-
1014
- return null
1015
- }
1016
- }
1017
-
1018
- const createCompileProxyService = ({ projectDirectoryUrl }) => {
1019
- const jsenvCompileProxyRelativeUrlForProject = urlToRelativeUrl(
1020
- jsenvCompileProxyFileInfo.jsenvBuildUrl,
1021
- projectDirectoryUrl,
1022
- )
1023
-
1024
- return (request) => {
1025
- if (request.ressource === "/.jsenv/jsenv_compile_proxy.js") {
1026
- const jsenvCompileProxyBuildServerUrl = `${request.origin}/${jsenvCompileProxyRelativeUrlForProject}`
1027
- return {
1028
- status: 307,
1029
- headers: {
1030
- location: jsenvCompileProxyBuildServerUrl,
1031
- },
1032
- }
1033
- }
1034
-
1035
- return null
1036
- }
1037
- }
1038
-
1039
- const readPackage = (packagePath) => {
1040
- const buffer = readFileSync(packagePath)
1041
- const string = String(buffer)
1042
- const packageObject = JSON.parse(string)
1043
- return packageObject
1044
- }
1045
-
1046
- const __filenameReplacement = `import.meta.url.slice('file:///'.length)`
1047
-
1048
- const __dirnameReplacement = `import.meta.url.slice('file:///'.length).replace(/[\\\/\\\\][^\\\/\\\\]*$/, '')`
1
+ import { readFileSync } from "node:fs"
2
+ import {
3
+ jsenvAccessControlAllowedHeaders,
4
+ startServer,
5
+ fetchFileSystem,
6
+ createSSERoom,
7
+ composeServicesWithTiming,
8
+ urlToContentType,
9
+ pluginServerTiming,
10
+ pluginRequestWaitingCheck,
11
+ pluginCORS,
12
+ } from "@jsenv/server"
13
+ import { createLogger, createDetailedMessage } from "@jsenv/logger"
14
+ import {
15
+ resolveUrl,
16
+ urlToFileSystemPath,
17
+ urlToRelativeUrl,
18
+ resolveDirectoryUrl,
19
+ readFile,
20
+ writeFile,
21
+ ensureEmptyDirectory,
22
+ registerDirectoryLifecycle,
23
+ urlIsInsideOf,
24
+ urlToBasename,
25
+ urlToExtension,
26
+ } from "@jsenv/filesystem"
27
+ import {
28
+ createCallbackList,
29
+ createCallbackListNotifiedOnce,
30
+ } from "@jsenv/abort"
31
+
32
+ import { isBrowserPartOfSupportedRuntimes } from "@jsenv/core/src/internal/generateGroupMap/runtime_support.js"
33
+ import { loadBabelPluginMapFromFile } from "./load_babel_plugin_map_from_file.js"
34
+ import { extractSyntaxBabelPluginMap } from "./babel_plugins.js"
35
+ import { generateGroupMap } from "../generateGroupMap/generateGroupMap.js"
36
+ import {
37
+ jsenvCompileProxyFileInfo,
38
+ sourcemapMainFileInfo,
39
+ sourcemapMappingFileInfo,
40
+ } from "../jsenvInternalFiles.js"
41
+ import { jsenvCoreDirectoryUrl } from "../jsenvCoreDirectoryUrl.js"
42
+ import { babelPluginReplaceExpressions } from "../babel_plugin_replace_expressions.js"
43
+ import { babelPluginGlobalThisAsJsenvImport } from "./babel_plugin_global_this_as_jsenv_import.js"
44
+ import { babelPluginNewStylesheetAsJsenvImport } from "./babel_plugin_new_stylesheet_as_jsenv_import.js"
45
+ import { babelPluginImportAssertions } from "./babel_plugin_import_assertions.js"
46
+ import { createCompiledFileService } from "./createCompiledFileService.js"
47
+ import { urlIsCompilationAsset } from "./compile-directory/compile-asset.js"
48
+ import { createTransformHtmlSourceFileService } from "./html_source_file_service.js"
49
+
50
+ export const startCompileServer = async ({
51
+ signal = new AbortController().signal,
52
+ handleSIGINT,
53
+ compileServerLogLevel,
54
+
55
+ projectDirectoryUrl,
56
+
57
+ importDefaultExtension,
58
+
59
+ jsenvDirectoryRelativeUrl = ".jsenv",
60
+ jsenvDirectoryClean = false,
61
+ outDirectoryName = "out",
62
+
63
+ sourcemapMethod = "comment", // "inline" is also possible
64
+ sourcemapExcludeSources = false, // this should increase perf (no need to download source for browser)
65
+ compileServerCanReadFromFilesystem = true,
66
+ compileServerCanWriteOnFilesystem = true,
67
+ compileCacheStrategy = "mtime",
68
+ projectFileCacheStrategy = "mtime",
69
+
70
+ // js compile options
71
+ transformTopLevelAwait = true,
72
+ moduleOutFormat = "systemjs",
73
+ importMetaFormat = moduleOutFormat,
74
+ env = {},
75
+ processEnvNodeEnv = process.env.NODE_ENV,
76
+ replaceProcessEnvNodeEnv = true,
77
+ replaceGlobalObject = false,
78
+ replaceGlobalFilename = false,
79
+ replaceGlobalDirname = false,
80
+ replaceMap = {},
81
+ babelPluginMap,
82
+ babelConfigFileUrl,
83
+ customCompilers = {},
84
+
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
+ // remaining options
96
+ runtimeSupport,
97
+
98
+ livereloadWatchConfig = {
99
+ "./**": true,
100
+ "./**/.*/": false, // any folder starting with a dot is ignored (includes .git for instance)
101
+ "./dist/": false,
102
+ "./**/node_modules/": false,
103
+ },
104
+ livereloadLogLevel = "info",
105
+ customServices = {},
106
+ livereloadSSE = false,
107
+ transformHtmlSourceFiles = true,
108
+ jsenvToolbarInjection = false,
109
+ jsenvScriptInjection = true,
110
+ inlineImportMapIntoHTML = true,
111
+ }) => {
112
+ assertArguments({
113
+ projectDirectoryUrl,
114
+ jsenvDirectoryRelativeUrl,
115
+ outDirectoryName,
116
+ })
117
+
118
+ const jsenvDirectoryUrl = resolveDirectoryUrl(
119
+ jsenvDirectoryRelativeUrl,
120
+ projectDirectoryUrl,
121
+ )
122
+ const outDirectoryUrl = resolveDirectoryUrl(
123
+ outDirectoryName,
124
+ jsenvDirectoryUrl,
125
+ )
126
+ const outDirectoryRelativeUrl = urlToRelativeUrl(
127
+ outDirectoryUrl,
128
+ projectDirectoryUrl,
129
+ )
130
+ // normalization
131
+ jsenvDirectoryRelativeUrl = urlToRelativeUrl(
132
+ jsenvDirectoryUrl,
133
+ projectDirectoryUrl,
134
+ )
135
+
136
+ const logger = createLogger({ logLevel: compileServerLogLevel })
137
+
138
+ const browser = isBrowserPartOfSupportedRuntimes(runtimeSupport)
139
+ const babelPluginMapFromFile = await loadBabelPluginMapFromFile({
140
+ projectDirectoryUrl,
141
+ babelConfigFileUrl,
142
+ })
143
+ babelPluginMap = {
144
+ "global-this-as-jsenv-import": babelPluginGlobalThisAsJsenvImport,
145
+ "new-stylesheet-as-jsenv-import": babelPluginNewStylesheetAsJsenvImport,
146
+ "transform-import-assertions": babelPluginImportAssertions,
147
+ ...babelPluginMapFromFile,
148
+ ...babelPluginMap,
149
+ }
150
+ Object.keys(babelPluginMap).forEach((key) => {
151
+ if (
152
+ key === "transform-modules-commonjs" ||
153
+ key === "transform-modules-amd" ||
154
+ key === "transform-modules-systemjs"
155
+ ) {
156
+ const declaredInFile = Boolean(babelPluginMapFromFile[key])
157
+ logger.warn(
158
+ createDetailedMessage(
159
+ `WARNING: "${key}" babel plugin should not be enabled, it will be ignored`,
160
+ {
161
+ suggestion: declaredInFile
162
+ ? `To get rid of this warning, remove "${key}" from babel config file. Either with "modules": false in @babel/preset-env or removing "@babel/${key}" from plugins`
163
+ : `To get rid of this warning, remove "${key}" from babelPluginMap parameter`,
164
+ },
165
+ ),
166
+ )
167
+ delete babelPluginMap[key]
168
+ }
169
+ })
170
+
171
+ const { babelSyntaxPluginMap, babelPluginMapWithoutSyntax } =
172
+ extractSyntaxBabelPluginMap(babelPluginMap)
173
+ const compileServerGroupMap = generateGroupMap({
174
+ babelPluginMap: babelPluginMapWithoutSyntax,
175
+ runtimeSupport,
176
+ })
177
+
178
+ babelPluginMap = {
179
+ // When code should be compatible with browsers, ensure
180
+ // process.env.NODE_ENV is replaced to be executable in a browser by forcing
181
+ // "transform-replace-expressions" babel plugin.
182
+ // It happens for module written in ESM but also using process.env.NODE_ENV
183
+ // for example "react-redux"
184
+ // This babel plugin won't force compilation because it's added after "generateGroupMap"
185
+ // however it will be used even if not part of "pluginRequiredNameArray"
186
+ // as visible in "babelPluginMapFromCompileId"
187
+ // This is a quick workaround to get things working because:
188
+ // - If none of your code needs to be compiled but one of your dependency
189
+ // uses process.env.NODE_ENV, the code will throw "process" is undefined
190
+ // This is fine but you won't have a dedicated way to force compilation to ensure
191
+ // "process.env.NODE_ENV" is replaced.
192
+ // Ideally this should be a custom compiler dedicated for this use case. It's not the case
193
+ // for now because it was faster to do it this way and the use case is a bit blurry:
194
+ // What should this custom compiler do? Just replace some node globals? How would it be named and documented?
195
+ ...(browser
196
+ ? {
197
+ "transform-replace-expressions": [
198
+ babelPluginReplaceExpressions,
199
+ {
200
+ replaceMap: {
201
+ ...(replaceProcessEnvNodeEnv
202
+ ? { "process.env.NODE_ENV": `("${processEnvNodeEnv}")` }
203
+ : {}),
204
+ ...(replaceGlobalObject ? { global: "globalThis" } : {}),
205
+ ...(replaceGlobalFilename
206
+ ? { __filename: __filenameReplacement }
207
+ : {}),
208
+ ...(replaceGlobalDirname
209
+ ? { __dirname: __dirnameReplacement }
210
+ : {}),
211
+ ...replaceMap,
212
+ },
213
+ allowConflictingReplacements: true,
214
+ },
215
+ ],
216
+ }
217
+ : {}),
218
+ ...babelSyntaxPluginMap,
219
+ ...babelPluginMap,
220
+ }
221
+
222
+ const serverStopCallbackList = createCallbackListNotifiedOnce()
223
+
224
+ let projectFileRequestedCallback = () => {}
225
+ if (livereloadSSE) {
226
+ const sseSetup = setupServerSentEventsForLivereload({
227
+ serverStopCallbackList,
228
+ projectDirectoryUrl,
229
+ jsenvDirectoryRelativeUrl,
230
+ outDirectoryRelativeUrl,
231
+ livereloadLogLevel,
232
+ livereloadWatchConfig,
233
+ })
234
+
235
+ projectFileRequestedCallback = sseSetup.projectFileRequestedCallback
236
+ const serveSSEForLivereload = createSSEForLivereloadService({
237
+ serverStopCallbackList,
238
+ outDirectoryRelativeUrl,
239
+ trackMainAndDependencies: sseSetup.trackMainAndDependencies,
240
+ })
241
+ customServices = {
242
+ "service:sse": serveSSEForLivereload,
243
+ ...customServices,
244
+ }
245
+ } else {
246
+ const roomWhenLivereloadIsDisabled = createSSERoom()
247
+ roomWhenLivereloadIsDisabled.open()
248
+ customServices = {
249
+ "service:sse": (request) => {
250
+ const { accept } = request.headers
251
+ if (!accept || !accept.includes("text/event-stream")) {
252
+ return null
253
+ }
254
+ return roomWhenLivereloadIsDisabled.join(request)
255
+ },
256
+ ...customServices,
257
+ }
258
+ }
259
+
260
+ const compileServerMetaFileInfo = createCompileServerMetaFileInfo({
261
+ projectDirectoryUrl,
262
+ jsenvDirectoryRelativeUrl,
263
+ outDirectoryRelativeUrl,
264
+ importDefaultExtension,
265
+ compileServerGroupMap,
266
+ env,
267
+ inlineImportMapIntoHTML,
268
+ babelPluginMap,
269
+ customCompilers,
270
+ jsenvToolbarInjection,
271
+ sourcemapMethod,
272
+ sourcemapExcludeSources,
273
+ })
274
+ if (compileServerCanWriteOnFilesystem) {
275
+ await cleanOutDirectoryIfNeeded({
276
+ logger,
277
+ outDirectoryUrl,
278
+ jsenvDirectoryUrl,
279
+ jsenvDirectoryClean,
280
+ compileServerMetaFileInfo,
281
+ })
282
+ writeFile(
283
+ compileServerMetaFileInfo.url,
284
+ JSON.stringify(compileServerMetaFileInfo.data, null, " "),
285
+ )
286
+ logger.debug(`-> ${compileServerMetaFileInfo.url}`)
287
+ }
288
+
289
+ const jsenvServices = {
290
+ "service:compilation asset": createCompilationAssetFileService({
291
+ projectDirectoryUrl,
292
+ }),
293
+ "service:compile server meta": createCompileServerMetaService({
294
+ projectDirectoryUrl,
295
+ outDirectoryUrl,
296
+ compileServerMetaFileInfo,
297
+ }),
298
+ "service: compile proxy": createCompileProxyService({
299
+ projectDirectoryUrl,
300
+ }),
301
+ "service:compiled file": createCompiledFileService({
302
+ logger,
303
+
304
+ projectDirectoryUrl,
305
+ outDirectoryRelativeUrl,
306
+
307
+ importDefaultExtension,
308
+
309
+ runtimeSupport,
310
+ transformTopLevelAwait,
311
+ groupMap: compileServerGroupMap,
312
+ babelPluginMap,
313
+ customCompilers,
314
+ moduleOutFormat,
315
+ importMetaFormat,
316
+ jsenvToolbarInjection,
317
+
318
+ projectFileRequestedCallback,
319
+ sourcemapMethod,
320
+ sourcemapExcludeSources,
321
+ compileCacheStrategy: compileServerCanReadFromFilesystem
322
+ ? compileCacheStrategy
323
+ : "none",
324
+ }),
325
+ ...(transformHtmlSourceFiles
326
+ ? {
327
+ "service:transform html source file":
328
+ createTransformHtmlSourceFileService({
329
+ logger,
330
+ projectDirectoryUrl,
331
+ inlineImportMapIntoHTML,
332
+ jsenvScriptInjection,
333
+ jsenvToolbarInjection,
334
+ }),
335
+ }
336
+ : {}),
337
+ "service:source file": createSourceFileService({
338
+ projectDirectoryUrl,
339
+ projectFileRequestedCallback,
340
+ projectFileCacheStrategy,
341
+ }),
342
+ }
343
+
344
+ const compileServer = await startServer({
345
+ signal,
346
+ stopOnExit: false,
347
+ stopOnSIGINT: handleSIGINT,
348
+ stopOnInternalError: false,
349
+ sendServerInternalErrorDetails: true,
350
+ keepProcessAlive,
351
+
352
+ logLevel: compileServerLogLevel,
353
+
354
+ protocol: compileServerProtocol,
355
+ http2: compileServerHttp2,
356
+ serverCertificate: compileServerCertificate,
357
+ serverCertificatePrivateKey: compileServerPrivateKey,
358
+ ip: compileServerIp,
359
+ port: compileServerPort,
360
+ plugins: {
361
+ ...pluginCORS({
362
+ accessControlAllowRequestOrigin: true,
363
+ accessControlAllowRequestMethod: true,
364
+ accessControlAllowRequestHeaders: true,
365
+ accessControlAllowedRequestHeaders: [
366
+ ...jsenvAccessControlAllowedHeaders,
367
+ "x-jsenv-execution-id",
368
+ ],
369
+ accessControlAllowCredentials: true,
370
+ }),
371
+ ...pluginServerTiming,
372
+ ...pluginRequestWaitingCheck({
373
+ requestWaitingMs: 60 * 1000,
374
+ }),
375
+ },
376
+ requestToResponse: composeServicesWithTiming({
377
+ ...customServices,
378
+ ...jsenvServices,
379
+ }),
380
+ onStop: (reason) => {
381
+ onStop()
382
+ serverStopCallbackList.notify(reason)
383
+ },
384
+ })
385
+
386
+ return {
387
+ jsenvDirectoryRelativeUrl,
388
+ outDirectoryRelativeUrl,
389
+ ...compileServer,
390
+ compileServerGroupMap,
391
+ babelPluginMap,
392
+ }
393
+ }
394
+
395
+ export const computeOutDirectoryRelativeUrl = ({
396
+ projectDirectoryUrl,
397
+ jsenvDirectoryRelativeUrl = ".jsenv",
398
+ outDirectoryName = "out",
399
+ }) => {
400
+ const jsenvDirectoryUrl = resolveDirectoryUrl(
401
+ jsenvDirectoryRelativeUrl,
402
+ projectDirectoryUrl,
403
+ )
404
+ const outDirectoryUrl = resolveDirectoryUrl(
405
+ outDirectoryName,
406
+ jsenvDirectoryUrl,
407
+ )
408
+ const outDirectoryRelativeUrl = urlToRelativeUrl(
409
+ outDirectoryUrl,
410
+ projectDirectoryUrl,
411
+ )
412
+
413
+ return outDirectoryRelativeUrl
414
+ }
415
+
416
+ const assertArguments = ({
417
+ projectDirectoryUrl,
418
+ jsenvDirectoryRelativeUrl,
419
+ outDirectoryName,
420
+ }) => {
421
+ if (typeof projectDirectoryUrl !== "string") {
422
+ throw new TypeError(
423
+ `projectDirectoryUrl must be a string. got ${projectDirectoryUrl}`,
424
+ )
425
+ }
426
+
427
+ if (typeof jsenvDirectoryRelativeUrl !== "string") {
428
+ throw new TypeError(
429
+ `jsenvDirectoryRelativeUrl must be a string. got ${jsenvDirectoryRelativeUrl}`,
430
+ )
431
+ }
432
+ const jsenvDirectoryUrl = resolveDirectoryUrl(
433
+ jsenvDirectoryRelativeUrl,
434
+ projectDirectoryUrl,
435
+ )
436
+
437
+ if (!jsenvDirectoryUrl.startsWith(projectDirectoryUrl)) {
438
+ throw new TypeError(
439
+ createDetailedMessage(
440
+ `jsenv directory must be inside project directory`,
441
+ {
442
+ ["jsenv directory url"]: jsenvDirectoryUrl,
443
+ ["project directory url"]: projectDirectoryUrl,
444
+ },
445
+ ),
446
+ )
447
+ }
448
+
449
+ if (typeof outDirectoryName !== "string") {
450
+ throw new TypeError(
451
+ `outDirectoryName must be a string. got ${outDirectoryName}`,
452
+ )
453
+ }
454
+ }
455
+
456
+ const cleanOutDirectoryIfNeeded = async ({
457
+ logger,
458
+ outDirectoryUrl,
459
+ jsenvDirectoryClean,
460
+ jsenvDirectoryUrl,
461
+ compileServerMetaFileInfo,
462
+ }) => {
463
+ if (jsenvDirectoryClean) {
464
+ logger.debug(
465
+ `Cleaning jsenv directory because jsenvDirectoryClean parameter enabled`,
466
+ )
467
+ await ensureEmptyDirectory(jsenvDirectoryUrl)
468
+ }
469
+
470
+ let previousCompileServerMeta
471
+ try {
472
+ const source = await readFile(compileServerMetaFileInfo.url)
473
+ previousCompileServerMeta = JSON.parse(source)
474
+ } catch (e) {
475
+ if (e.code === "ENOENT") {
476
+ previousCompileServerMeta = null
477
+ } else {
478
+ throw e
479
+ }
480
+ }
481
+
482
+ if (previousCompileServerMeta !== null) {
483
+ const outDirectoryChanges = getOutDirectoryChanges(
484
+ previousCompileServerMeta,
485
+ compileServerMetaFileInfo.data,
486
+ )
487
+
488
+ if (outDirectoryChanges) {
489
+ if (!jsenvDirectoryClean) {
490
+ logger.debug(
491
+ createDetailedMessage(
492
+ `Cleaning jsenv ${urlToBasename(
493
+ outDirectoryUrl.slice(0, -1),
494
+ )} directory because configuration has changed.`,
495
+ {
496
+ "changes": outDirectoryChanges.namedChanges
497
+ ? outDirectoryChanges.namedChanges
498
+ : `something`,
499
+ "out directory": urlToFileSystemPath(outDirectoryUrl),
500
+ },
501
+ ),
502
+ )
503
+ }
504
+ await ensureEmptyDirectory(outDirectoryUrl)
505
+ }
506
+ }
507
+ }
508
+
509
+ const getOutDirectoryChanges = (
510
+ previousCompileServerMeta,
511
+ compileServerMeta,
512
+ ) => {
513
+ const changes = []
514
+
515
+ Object.keys(compileServerMeta).forEach((key) => {
516
+ const now = compileServerMeta[key]
517
+ const previous = previousCompileServerMeta[key]
518
+ if (!compareValueJson(now, previous)) {
519
+ changes.push(key)
520
+ }
521
+ })
522
+
523
+ if (changes.length > 0) {
524
+ return { namedChanges: changes }
525
+ }
526
+
527
+ // in case basic comparison from above is not enough
528
+ if (!compareValueJson(previousCompileServerMeta, compileServerMeta)) {
529
+ return { somethingChanged: true }
530
+ }
531
+
532
+ return null
533
+ }
534
+
535
+ const compareValueJson = (left, right) => {
536
+ return JSON.stringify(left) === JSON.stringify(right)
537
+ }
538
+
539
+ /**
540
+ * We need to get two things:
541
+ * { projectFileRequestedCallback, trackMainAndDependencies }
542
+ *
543
+ * projectFileRequestedCallback
544
+ * This function will be called by the compile server every time a file inside projectDirectory
545
+ * is requested so that we can build up the dependency tree of any file
546
+ *
547
+ * trackMainAndDependencies
548
+ * This function is meant to be used to implement server sent events in order for a client to know
549
+ * when a given file or any of its dependencies changes in order to implement livereloading.
550
+ * At any time this function can be called with (mainRelativeUrl, { modified, removed, lastEventId })
551
+ * modified is called
552
+ * - immediatly if lastEventId is passed and mainRelativeUrl or any of its dependencies have
553
+ * changed since that event (last change is passed to modified if their is more than one change)
554
+ * - when mainRelativeUrl or any of its dependencies is modified
555
+ * removed is called
556
+ * - with same spec as modified but when a file is deleted from the filesystem instead of modified
557
+ *
558
+ */
559
+ const setupServerSentEventsForLivereload = ({
560
+ serverStopCallbackList,
561
+ projectDirectoryUrl,
562
+ jsenvDirectoryRelativeUrl,
563
+ outDirectoryRelativeUrl,
564
+ livereloadLogLevel,
565
+ livereloadWatchConfig,
566
+ }) => {
567
+ const livereloadLogger = createLogger({ logLevel: livereloadLogLevel })
568
+ const trackerMap = new Map()
569
+ const projectFileRequested = createCallbackList()
570
+ const projectFileModified = createCallbackList()
571
+ const projectFileRemoved = createCallbackList()
572
+ const projectFileAdded = createCallbackList()
573
+
574
+ const projectFileRequestedCallback = (relativeUrl, request) => {
575
+ const url = `${projectDirectoryUrl}${relativeUrl}`
576
+
577
+ if (
578
+ // Do not watch sourcemap files
579
+ urlToExtension(url) === ".map" ||
580
+ // Do not watch compilation asset, watching source file is enough
581
+ urlIsCompilationAsset(url)
582
+ ) {
583
+ return
584
+ }
585
+
586
+ projectFileRequested.notify({ relativeUrl, request })
587
+ }
588
+ const watchDescription = {
589
+ ...livereloadWatchConfig,
590
+ [jsenvDirectoryRelativeUrl]: false,
591
+ }
592
+ const unregisterDirectoryLifecyle = registerDirectoryLifecycle(
593
+ projectDirectoryUrl,
594
+ {
595
+ watchDescription,
596
+ updated: ({ relativeUrl }) => {
597
+ projectFileModified.notify(relativeUrl)
598
+ },
599
+ removed: ({ relativeUrl }) => {
600
+ projectFileRemoved.notify(relativeUrl)
601
+ },
602
+ added: ({ relativeUrl }) => {
603
+ projectFileAdded.notify(relativeUrl)
604
+ },
605
+ keepProcessAlive: false,
606
+ recursive: true,
607
+ },
608
+ )
609
+ serverStopCallbackList.add(unregisterDirectoryLifecyle)
610
+
611
+ const getDependencySet = (mainRelativeUrl) => {
612
+ if (trackerMap.has(mainRelativeUrl)) {
613
+ return trackerMap.get(mainRelativeUrl)
614
+ }
615
+ const dependencySet = new Set()
616
+ dependencySet.add(mainRelativeUrl)
617
+ trackerMap.set(mainRelativeUrl, dependencySet)
618
+ return dependencySet
619
+ }
620
+
621
+ // each time a file is requested for the first time its dependencySet is computed
622
+ projectFileRequested.add(({ relativeUrl: mainRelativeUrl }) => {
623
+ // for now no use case of livereloading on node.js
624
+ // and for browsers only html file can be main files
625
+ // this avoid collecting dependencies of non html files that will never be used
626
+ if (!mainRelativeUrl.endsWith(".html")) {
627
+ return
628
+ }
629
+
630
+ // when a file is requested, always rebuild its dependency in case it has changed
631
+ // since the last time it was requested
632
+ const dependencySet = new Set()
633
+ dependencySet.add(mainRelativeUrl)
634
+ trackerMap.set(mainRelativeUrl, dependencySet)
635
+
636
+ const removeDependencyRequestedCallback = projectFileRequested.add(
637
+ ({ relativeUrl, request }) => {
638
+ if (dependencySet.has(relativeUrl)) {
639
+ return
640
+ }
641
+
642
+ const dependencyReport = reportDependency(
643
+ relativeUrl,
644
+ mainRelativeUrl,
645
+ request,
646
+ )
647
+ if (dependencyReport.dependency === false) {
648
+ livereloadLogger.debug(
649
+ `${relativeUrl} not a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
650
+ )
651
+ return
652
+ }
653
+
654
+ livereloadLogger.debug(
655
+ `${relativeUrl} is a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
656
+ )
657
+ dependencySet.add(relativeUrl)
658
+ },
659
+ )
660
+ const removeMainRemovedCallback = projectFileRemoved.add((relativeUrl) => {
661
+ if (relativeUrl === mainRelativeUrl) {
662
+ removeDependencyRequestedCallback()
663
+ removeMainRemovedCallback()
664
+ trackerMap.delete(mainRelativeUrl)
665
+ }
666
+ })
667
+ })
668
+
669
+ const trackMainAndDependencies = (
670
+ mainRelativeUrl,
671
+ { modified, removed, added },
672
+ ) => {
673
+ livereloadLogger.debug(`track ${mainRelativeUrl} and its dependencies`)
674
+
675
+ const removeModifiedCallback = projectFileModified.add((relativeUrl) => {
676
+ const dependencySet = getDependencySet(mainRelativeUrl)
677
+ if (dependencySet.has(relativeUrl)) {
678
+ modified(relativeUrl)
679
+ }
680
+ })
681
+ const removeRemovedCallback = projectFileRemoved.add((relativeUrl) => {
682
+ const dependencySet = getDependencySet(mainRelativeUrl)
683
+ if (dependencySet.has(relativeUrl)) {
684
+ removed(relativeUrl)
685
+ }
686
+ })
687
+ const removeAddedCallback = projectFileAdded.add((relativeUrl) => {
688
+ const dependencySet = getDependencySet(mainRelativeUrl)
689
+ if (dependencySet.has(relativeUrl)) {
690
+ added(relativeUrl)
691
+ }
692
+ })
693
+
694
+ return () => {
695
+ livereloadLogger.debug(
696
+ `stop tracking ${mainRelativeUrl} and its dependencies.`,
697
+ )
698
+ removeModifiedCallback()
699
+ removeRemovedCallback()
700
+ removeAddedCallback()
701
+ }
702
+ }
703
+
704
+ const reportDependency = (relativeUrl, mainRelativeUrl, request) => {
705
+ if (relativeUrl === mainRelativeUrl) {
706
+ return {
707
+ dependency: true,
708
+ reason: "it's main",
709
+ }
710
+ }
711
+
712
+ if ("x-jsenv-execution-id" in request.headers) {
713
+ const executionId = request.headers["x-jsenv-execution-id"]
714
+ if (executionId === mainRelativeUrl) {
715
+ return {
716
+ dependency: true,
717
+ reason: "x-jsenv-execution-id request header",
718
+ }
719
+ }
720
+ return {
721
+ dependency: false,
722
+ reason: "x-jsenv-execution-id request header",
723
+ }
724
+ }
725
+
726
+ const { referer } = request.headers
727
+ if (referer) {
728
+ const { origin } = request
729
+ // referer is likely the exploringServer
730
+ if (referer !== origin && !urlIsInsideOf(referer, origin)) {
731
+ return {
732
+ dependency: false,
733
+ reason: "referer is an other origin",
734
+ }
735
+ }
736
+ // here we know the referer is inside compileServer
737
+ const refererRelativeUrl = urlToOriginalRelativeUrl(
738
+ referer,
739
+ resolveUrl(outDirectoryRelativeUrl, request.origin),
740
+ )
741
+ if (refererRelativeUrl) {
742
+ // search if referer (file requesting this one) is tracked as being a dependency of main file
743
+ // in that case because the importer is a dependency the importee is also a dependency
744
+ // eslint-disable-next-line no-unused-vars
745
+ for (const tracker of trackerMap) {
746
+ if (
747
+ tracker[0] === mainRelativeUrl &&
748
+ tracker[1].has(refererRelativeUrl)
749
+ ) {
750
+ return {
751
+ dependency: true,
752
+ reason: "referer is a dependency",
753
+ }
754
+ }
755
+ }
756
+ }
757
+ }
758
+
759
+ return {
760
+ dependency: true,
761
+ reason: "it was requested",
762
+ }
763
+ }
764
+
765
+ return {
766
+ projectFileRequestedCallback,
767
+ trackMainAndDependencies,
768
+ }
769
+ }
770
+
771
+ const createSSEForLivereloadService = ({
772
+ serverStopCallbackList,
773
+ outDirectoryRelativeUrl,
774
+ trackMainAndDependencies,
775
+ }) => {
776
+ const cache = []
777
+ const sseRoomLimit = 100
778
+ const getOrCreateSSERoom = (mainFileRelativeUrl) => {
779
+ const cacheEntry = cache.find(
780
+ (cacheEntryCandidate) =>
781
+ cacheEntryCandidate.mainFileRelativeUrl === mainFileRelativeUrl,
782
+ )
783
+ if (cacheEntry) {
784
+ return cacheEntry.sseRoom
785
+ }
786
+
787
+ const sseRoom = createSSERoom({
788
+ retryDuration: 2000,
789
+ historyLength: 100,
790
+ welcomeEventEnabled: true,
791
+ })
792
+
793
+ // each time something is modified or removed we send event to the room
794
+ const stopTracking = trackMainAndDependencies(mainFileRelativeUrl, {
795
+ modified: (relativeUrl) => {
796
+ sseRoom.sendEvent({ type: "file-modified", data: relativeUrl })
797
+ },
798
+ removed: (relativeUrl) => {
799
+ sseRoom.sendEvent({ type: "file-removed", data: relativeUrl })
800
+ },
801
+ added: (relativeUrl) => {
802
+ sseRoom.sendEvent({ type: "file-added", data: relativeUrl })
803
+ },
804
+ })
805
+
806
+ const removeSSECleanupCallback = serverStopCallbackList.add(() => {
807
+ removeSSECleanupCallback()
808
+ sseRoom.close()
809
+ stopTracking()
810
+ })
811
+ cache.push({
812
+ mainFileRelativeUrl,
813
+ sseRoom,
814
+ cleanup: () => {
815
+ removeSSECleanupCallback()
816
+ sseRoom.close()
817
+ stopTracking()
818
+ },
819
+ })
820
+ if (cache.length >= sseRoomLimit) {
821
+ const firstCacheEntry = cache.shift()
822
+ firstCacheEntry.cleanup()
823
+ }
824
+ return sseRoom
825
+ }
826
+
827
+ return (request) => {
828
+ const { accept } = request.headers
829
+ if (!accept || !accept.includes("text/event-stream")) {
830
+ return null
831
+ }
832
+
833
+ const fileRelativeUrl = urlToOriginalRelativeUrl(
834
+ resolveUrl(request.ressource, request.origin),
835
+ resolveUrl(outDirectoryRelativeUrl, request.origin),
836
+ )
837
+
838
+ const room = getOrCreateSSERoom(fileRelativeUrl)
839
+ return room.join(request)
840
+ }
841
+ }
842
+
843
+ const urlToOriginalRelativeUrl = (url, outDirectoryRemoteUrl) => {
844
+ if (urlIsInsideOf(url, outDirectoryRemoteUrl)) {
845
+ const afterCompileDirectory = urlToRelativeUrl(url, outDirectoryRemoteUrl)
846
+ const fileRelativeUrl = afterCompileDirectory.slice(
847
+ afterCompileDirectory.indexOf("/") + 1,
848
+ )
849
+ return fileRelativeUrl
850
+ }
851
+ return new URL(url).pathname.slice(1)
852
+ }
853
+
854
+ const createCompilationAssetFileService = ({ projectDirectoryUrl }) => {
855
+ return (request) => {
856
+ const { origin, ressource } = request
857
+ const requestUrl = `${origin}${ressource}`
858
+ if (urlIsCompilationAsset(requestUrl)) {
859
+ return fetchFileSystem(
860
+ new URL(request.ressource.slice(1), projectDirectoryUrl),
861
+ {
862
+ headers: request.headers,
863
+ etagEnabled: true,
864
+ },
865
+ )
866
+ }
867
+ return null
868
+ }
869
+ }
870
+
871
+ const createSourceFileService = ({
872
+ projectDirectoryUrl,
873
+ projectFileRequestedCallback,
874
+ projectFileCacheStrategy,
875
+ }) => {
876
+ return async (request) => {
877
+ const { ressource } = request
878
+ const relativeUrl = ressource.slice(1)
879
+ projectFileRequestedCallback(relativeUrl, request)
880
+
881
+ const responsePromise = fetchFileSystem(
882
+ new URL(request.ressource.slice(1), projectDirectoryUrl),
883
+ {
884
+ headers: request.headers,
885
+ etagEnabled: projectFileCacheStrategy === "etag",
886
+ mtimeEnabled: projectFileCacheStrategy === "mtime",
887
+ },
888
+ )
889
+
890
+ return responsePromise
891
+ }
892
+ }
893
+
894
+ const createCompileServerMetaFileInfo = ({
895
+ projectDirectoryUrl,
896
+ jsenvDirectoryRelativeUrl,
897
+ outDirectoryRelativeUrl,
898
+ importDefaultExtension,
899
+ compileServerGroupMap,
900
+ babelPluginMap,
901
+ replaceProcessEnvNodeEnv,
902
+ processEnvNodeEnv,
903
+ env,
904
+ inlineImportMapIntoHTML,
905
+ customCompilers,
906
+ jsenvToolbarInjection,
907
+ sourcemapMethod,
908
+ sourcemapExcludeSources,
909
+ }) => {
910
+ const outDirectoryUrl = resolveUrl(
911
+ outDirectoryRelativeUrl,
912
+ projectDirectoryUrl,
913
+ )
914
+ const compileServerMetaFileUrl = resolveUrl(
915
+ "./__compile_server_meta__.json",
916
+ outDirectoryUrl,
917
+ )
918
+ const jsenvCorePackageFileUrl = resolveUrl(
919
+ "./package.json",
920
+ jsenvCoreDirectoryUrl,
921
+ )
922
+ const jsenvCorePackageFilePath = urlToFileSystemPath(jsenvCorePackageFileUrl)
923
+ const jsenvCorePackageVersion = readPackage(jsenvCorePackageFilePath).version
924
+ const customCompilerPatterns = Object.keys(customCompilers)
925
+ const sourcemapMainFileRelativeUrl = urlToRelativeUrl(
926
+ sourcemapMainFileInfo.url,
927
+ projectDirectoryUrl,
928
+ )
929
+ const sourcemapMappingFileRelativeUrl = urlToRelativeUrl(
930
+ sourcemapMappingFileInfo.url,
931
+ projectDirectoryUrl,
932
+ )
933
+ const compileServerMeta = {
934
+ jsenvDirectoryRelativeUrl,
935
+ outDirectoryRelativeUrl,
936
+ importDefaultExtension,
937
+
938
+ babelPluginMap: babelPluginMapAsData(babelPluginMap),
939
+ compileServerGroupMap,
940
+ customCompilerPatterns,
941
+ replaceProcessEnvNodeEnv,
942
+ processEnvNodeEnv,
943
+ inlineImportMapIntoHTML,
944
+
945
+ sourcemapMethod,
946
+ sourcemapExcludeSources,
947
+ sourcemapMainFileRelativeUrl,
948
+ sourcemapMappingFileRelativeUrl,
949
+ errorStackRemapping: true,
950
+
951
+ jsenvCorePackageVersion,
952
+ jsenvToolbarInjection,
953
+ env,
954
+ }
955
+ return {
956
+ url: compileServerMetaFileUrl,
957
+ data: compileServerMeta,
958
+ }
959
+ }
960
+
961
+ const babelPluginMapAsData = (babelPluginMap) => {
962
+ const data = {}
963
+ Object.keys(babelPluginMap).forEach((key) => {
964
+ const value = babelPluginMap[key]
965
+ if (Array.isArray(value)) {
966
+ data[key] = value
967
+ return
968
+ }
969
+ if (typeof value === "object") {
970
+ data[key] = {
971
+ options: value.options,
972
+ }
973
+ return
974
+ }
975
+ data[key] = value
976
+ })
977
+ return data
978
+ }
979
+
980
+ const createCompileServerMetaService = ({
981
+ projectDirectoryUrl,
982
+ outDirectoryUrl,
983
+ compileServerMetaFileInfo,
984
+ }) => {
985
+ const isCompileServerMetaFile = (url) => {
986
+ if (!urlIsInsideOf(url, outDirectoryUrl)) {
987
+ return false
988
+ }
989
+ const afterOutDirectory = url.slice(outDirectoryUrl.length)
990
+ if (afterOutDirectory.indexOf("/") > -1) {
991
+ return false
992
+ }
993
+ return true
994
+ }
995
+
996
+ // serve from memory
997
+ return (request) => {
998
+ const requestUrl = resolveUrl(
999
+ request.ressource.slice(1),
1000
+ projectDirectoryUrl,
1001
+ )
1002
+ if (
1003
+ isCompileServerMetaFile(requestUrl) ||
1004
+ // allow to request it directly from .jsenv
1005
+ request.ressource === "/.jsenv/__compile_server_meta__.json"
1006
+ ) {
1007
+ const body = JSON.stringify(compileServerMetaFileInfo.data, null, " ")
1008
+ return {
1009
+ status: 200,
1010
+ headers: {
1011
+ "content-type": urlToContentType(requestUrl),
1012
+ "content-length": Buffer.byteLength(body),
1013
+ },
1014
+ body,
1015
+ }
1016
+ }
1017
+
1018
+ return null
1019
+ }
1020
+ }
1021
+
1022
+ const createCompileProxyService = ({ projectDirectoryUrl }) => {
1023
+ const jsenvCompileProxyRelativeUrlForProject = urlToRelativeUrl(
1024
+ jsenvCompileProxyFileInfo.jsenvBuildUrl,
1025
+ projectDirectoryUrl,
1026
+ )
1027
+
1028
+ return (request) => {
1029
+ if (request.ressource === "/.jsenv/jsenv_compile_proxy.js") {
1030
+ const jsenvCompileProxyBuildServerUrl = `${request.origin}/${jsenvCompileProxyRelativeUrlForProject}`
1031
+ return {
1032
+ status: 307,
1033
+ headers: {
1034
+ location: jsenvCompileProxyBuildServerUrl,
1035
+ },
1036
+ }
1037
+ }
1038
+
1039
+ return null
1040
+ }
1041
+ }
1042
+
1043
+ const readPackage = (packagePath) => {
1044
+ const buffer = readFileSync(packagePath)
1045
+ const string = String(buffer)
1046
+ const packageObject = JSON.parse(string)
1047
+ return packageObject
1048
+ }
1049
+
1050
+ const __filenameReplacement = `import.meta.url.slice('file:///'.length)`
1051
+
1052
+ const __dirnameReplacement = `import.meta.url.slice('file:///'.length).replace(/[\\\/\\\\][^\\\/\\\\]*$/, '')`