@jsenv/core 23.8.2 → 23.8.7

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