@jsenv/core 23.6.2 → 23.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/{license → LICENSE} +21 -21
  2. package/dist/jsenv_browser_system.js +35 -35
  3. package/dist/jsenv_browser_system.js.map +14 -14
  4. package/dist/jsenv_compile_proxy.js +47 -128
  5. package/dist/jsenv_compile_proxy.js.map +25 -62
  6. package/dist/jsenv_exploring_index.js.map +5 -5
  7. package/dist/jsenv_exploring_redirector.js +47 -54
  8. package/dist/jsenv_exploring_redirector.js.map +17 -19
  9. package/dist/jsenv_toolbar.js +47 -128
  10. package/dist/jsenv_toolbar.js.map +12 -47
  11. package/dist/jsenv_toolbar_injector.js.map +5 -5
  12. package/helpers/babel/.eslintrc.cjs +24 -24
  13. package/helpers/babel/AsyncGenerator/AsyncGenerator.js +81 -81
  14. package/helpers/babel/AwaitValue/AwaitValue.js +3 -3
  15. package/helpers/babel/applyDecoratorDescriptor/applyDecoratorDescriptor.js +33 -33
  16. package/helpers/babel/arrayLikeToArray/arrayLikeToArray.js +7 -7
  17. package/helpers/babel/arrayWithHoles/arrayWithHoles.js +4 -4
  18. package/helpers/babel/arrayWithoutHoles/arrayWithoutHoles.js +6 -6
  19. package/helpers/babel/assertThisInitialized/assertThisInitialized.js +7 -7
  20. package/helpers/babel/asyncGeneratorDelegate/asyncGeneratorDelegate.js +40 -40
  21. package/helpers/babel/asyncIterator/asyncIterator.js +12 -12
  22. package/helpers/babel/asyncToGenerator/asyncToGenerator.js +34 -34
  23. package/helpers/babel/awaitAsyncGenerator/awaitAsyncGenerator.js +5 -5
  24. package/helpers/babel/classApplyDescriptorDestructureSet/classApplyDescriptorDestructureSet.js +20 -20
  25. package/helpers/babel/classApplyDescriptorGet/classApplyDescriptorGet.js +6 -6
  26. package/helpers/babel/classApplyDescriptorSet/classApplyDescriptorSet.js +13 -13
  27. package/helpers/babel/classCallCheck/classCallCheck.js +5 -5
  28. package/helpers/babel/classCheckPrivateStaticAccess/classCheckPrivateStaticAccess.js +5 -5
  29. package/helpers/babel/classCheckPrivateStaticFieldDescriptor/classCheckPrivateStaticFieldDescriptor.js +6 -6
  30. package/helpers/babel/classExtractFieldDescriptor/classExtractFieldDescriptor.js +7 -7
  31. package/helpers/babel/classNameTDZError/classNameTDZError.js +4 -4
  32. package/helpers/babel/classPrivateFieldDestructureSet/classPrivateFieldDestructureSet.js +7 -7
  33. package/helpers/babel/classPrivateFieldGet/classPrivateFieldGet.js +7 -7
  34. package/helpers/babel/classPrivateFieldLooseBase/classPrivateFieldLooseBase.js +6 -6
  35. package/helpers/babel/classPrivateFieldLooseKey/classPrivateFieldLooseKey.js +5 -5
  36. package/helpers/babel/classPrivateFieldSet/classPrivateFieldSet.js +8 -8
  37. package/helpers/babel/classPrivateMethodGet/classPrivateMethodGet.js +6 -6
  38. package/helpers/babel/classPrivateMethodSet/classPrivateMethodSet.js +3 -3
  39. package/helpers/babel/classStaticPrivateFieldSpecGet/classStaticPrivateFieldSpecGet.js +9 -9
  40. package/helpers/babel/classStaticPrivateFieldSpecSet/classStaticPrivateFieldSpecSet.js +15 -15
  41. package/helpers/babel/classStaticPrivateMethodGet/classStaticPrivateMethodGet.js +6 -6
  42. package/helpers/babel/classStaticPrivateMethodSet/classStaticPrivateMethodSet.js +3 -3
  43. package/helpers/babel/construct/construct.js +16 -16
  44. package/helpers/babel/createClass/createClass.js +15 -15
  45. package/helpers/babel/createForOfIteratorHelper/createForOfIteratorHelper.js +60 -60
  46. package/helpers/babel/createForOfIteratorHelperLoose/createForOfIteratorHelperLoose.js +23 -23
  47. package/helpers/babel/createRawReactElement/createRawReactElement.js +50 -50
  48. package/helpers/babel/createSuper/createSuper.js +22 -22
  49. package/helpers/babel/decorate/decorate.js +403 -403
  50. package/helpers/babel/defaults/defaults.js +11 -11
  51. package/helpers/babel/defineEnumerableProperties/defineEnumerableProperties.js +23 -23
  52. package/helpers/babel/defineProperty/defineProperty.js +18 -18
  53. package/helpers/babel/extends/extends.js +14 -14
  54. package/helpers/babel/get/get.js +13 -13
  55. package/helpers/babel/getPrototypeOf/getPrototypeOf.js +4 -4
  56. package/helpers/babel/inherits/inherits.js +15 -15
  57. package/helpers/babel/inheritsLoose/inheritsLoose.js +7 -7
  58. package/helpers/babel/initializerDefineProperty/initializerDefineProperty.js +10 -10
  59. package/helpers/babel/initializerWarningHelper/initializerWarningHelper.js +6 -6
  60. package/helpers/babel/instanceof/instanceof.js +6 -6
  61. package/helpers/babel/interopRequireDefault/interopRequireDefault.js +3 -3
  62. package/helpers/babel/interopRequireWildcard/interopRequireWildcard.js +37 -37
  63. package/helpers/babel/isNativeFunction/isNativeFunction.js +4 -4
  64. package/helpers/babel/isNativeReflectConstruct/isNativeReflectConstruct.js +21 -21
  65. package/helpers/babel/iterableToArray/iterableToArray.js +7 -7
  66. package/helpers/babel/iterableToArrayLimit/iterableToArrayLimit.js +36 -36
  67. package/helpers/babel/iterableToArrayLimitLoose/iterableToArrayLimitLoose.js +10 -10
  68. package/helpers/babel/jsx/jsx.js +45 -45
  69. package/helpers/babel/maybeArrayLike/maybeArrayLike.js +10 -10
  70. package/helpers/babel/newArrowCheck/newArrowCheck.js +5 -5
  71. package/helpers/babel/nonIterableRest/nonIterableRest.js +5 -5
  72. package/helpers/babel/nonIterableSpread/nonIterableSpread.js +5 -5
  73. package/helpers/babel/objectDestructuringEmpty/objectDestructuringEmpty.js +3 -3
  74. package/helpers/babel/objectSpread/objectSpread.js +23 -23
  75. package/helpers/babel/objectSpread2/objectSpread2.js +33 -33
  76. package/helpers/babel/objectWithoutProperties/objectWithoutProperties.js +19 -19
  77. package/helpers/babel/objectWithoutPropertiesLoose/objectWithoutPropertiesLoose.js +13 -13
  78. package/helpers/babel/possibleConstructorReturn/possibleConstructorReturn.js +10 -10
  79. package/helpers/babel/readOnlyError/readOnlyError.js +4 -4
  80. package/helpers/babel/readme.md +9 -9
  81. package/helpers/babel/set/set.js +44 -44
  82. package/helpers/babel/setPrototypeOf/setPrototypeOf.js +6 -6
  83. package/helpers/babel/skipFirstGeneratorNext/skipFirstGeneratorNext.js +8 -8
  84. package/helpers/babel/slicedToArray/slicedToArray.js +10 -10
  85. package/helpers/babel/slicedToArrayLoose/slicedToArrayLoose.js +13 -13
  86. package/helpers/babel/superPropBase/superPropBase.js +10 -10
  87. package/helpers/babel/taggedTemplateLiteral/taggedTemplateLiteral.js +10 -10
  88. package/helpers/babel/taggedTemplateLiteralLoose/taggedTemplateLiteralLoose.js +7 -7
  89. package/helpers/babel/tdz/tdz.js +4 -4
  90. package/helpers/babel/temporalRef/temporalRef.js +6 -6
  91. package/helpers/babel/temporalUndefined/temporalUndefined.js +3 -3
  92. package/helpers/babel/toArray/toArray.js +10 -10
  93. package/helpers/babel/toConsumableArray/toConsumableArray.js +10 -10
  94. package/helpers/babel/toPrimitive/toPrimitive.js +10 -10
  95. package/helpers/babel/toPropertyKey/toPropertyKey.js +6 -6
  96. package/helpers/babel/typeof/typeof.js +14 -14
  97. package/helpers/babel/unsupportedIterableToArray/unsupportedIterableToArray.js +12 -12
  98. package/helpers/babel/wrapAsyncGenerator/wrapAsyncGenerator.js +8 -8
  99. package/helpers/babel/wrapNativeSuper/wrapNativeSuper.js +30 -30
  100. package/helpers/babel/wrapRegExp/wrapRegExp.js +63 -63
  101. package/helpers/babel/writeOnlyError/writeOnlyError.js +4 -4
  102. package/helpers/regenerator-runtime/regenerator-runtime.js +748 -748
  103. package/package.json +4 -2
  104. package/src/buildProject.js +300 -300
  105. package/src/execute.js +184 -184
  106. package/src/internal/browser-launcher/jsenv-browser-system.js +199 -199
  107. package/src/internal/compiling/createCompiledFileService.js +290 -290
  108. package/src/internal/compiling/startCompileServer.js +1052 -1131
  109. package/src/internal/executing/coverage/babel_plugin_instrument.js +90 -90
  110. package/src/internal/executing/coverage/reportToCoverage.js +187 -187
  111. package/src/internal/executing/executePlan.js +183 -183
  112. package/src/internal/executing/launchAndExecute.js +458 -450
  113. package/src/internal/runtime/createBrowserRuntime/scanBrowserRuntimeFeatures.js +246 -250
  114. package/src/internal/runtime/createNodeRuntime/scanNodeRuntimeFeatures.js +112 -115
  115. package/src/internal/runtime/s.js +727 -727
  116. package/src/internal/toolbar/jsenv-logo.svg +144 -144
  117. package/src/internal/url_conversion.js +317 -317
  118. package/src/launchNode.js +210 -210
  119. package/src/startExploring.js +309 -309
@@ -1,1131 +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 = "etag",
67
- projectFileEtagEnabled = true,
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 outJSONFiles = createOutJSONFiles({
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 setupOutDirectory({
275
- logger,
276
- outDirectoryMeta: outJSONFiles.meta.data,
277
- outDirectoryUrl,
278
- jsenvDirectoryUrl,
279
- jsenvDirectoryClean,
280
- })
281
- }
282
-
283
- const jsenvServices = {
284
- "service:compilation asset": createCompilationAssetFileService({
285
- projectDirectoryUrl,
286
- }),
287
- "service:browser script": createBrowserScriptService({
288
- projectDirectoryUrl,
289
- outDirectoryRelativeUrl,
290
- }),
291
- "service:out files": await createOutFilesService({
292
- logger,
293
- projectDirectoryUrl,
294
- compileServerCanWriteOnFilesystem,
295
- outDirectoryUrl,
296
- outJSONFiles,
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
- logger,
339
- projectDirectoryUrl,
340
- projectFileRequestedCallback,
341
- projectFileEtagEnabled,
342
- transformHtmlSourceFiles,
343
- inlineImportMapIntoHTML,
344
- jsenvScriptInjection,
345
- jsenvToolbarInjection,
346
- }),
347
- }
348
-
349
- const compileServer = await startServer({
350
- signal,
351
- stopOnExit: false,
352
- stopOnSIGINT: handleSIGINT,
353
- stopOnInternalError: false,
354
- sendServerInternalErrorDetails: true,
355
- keepProcessAlive,
356
-
357
- logLevel: compileServerLogLevel,
358
-
359
- protocol: compileServerProtocol,
360
- http2: compileServerHttp2,
361
- serverCertificate: compileServerCertificate,
362
- serverCertificatePrivateKey: compileServerPrivateKey,
363
- ip: compileServerIp,
364
- port: compileServerPort,
365
- plugins: {
366
- ...pluginCORS({
367
- accessControlAllowRequestOrigin: true,
368
- accessControlAllowRequestMethod: true,
369
- accessControlAllowRequestHeaders: true,
370
- accessControlAllowedRequestHeaders: [
371
- ...jsenvAccessControlAllowedHeaders,
372
- "x-jsenv-execution-id",
373
- ],
374
- accessControlAllowCredentials: true,
375
- }),
376
- ...pluginServerTiming,
377
- ...pluginRequestWaitingCheck({
378
- requestWaitingMs: 60 * 1000,
379
- }),
380
- },
381
- requestToResponse: composeServicesWithTiming({
382
- ...customServices,
383
- ...jsenvServices,
384
- }),
385
- onStop: (reason) => {
386
- onStop()
387
- serverStopCallbackList.notify(reason)
388
- },
389
- })
390
-
391
- return {
392
- jsenvDirectoryRelativeUrl,
393
- outDirectoryRelativeUrl,
394
- ...compileServer,
395
- compileServerGroupMap,
396
- babelPluginMap,
397
- }
398
- }
399
-
400
- export const computeOutDirectoryRelativeUrl = ({
401
- projectDirectoryUrl,
402
- jsenvDirectoryRelativeUrl = ".jsenv",
403
- outDirectoryName = "out",
404
- }) => {
405
- const jsenvDirectoryUrl = resolveDirectoryUrl(
406
- jsenvDirectoryRelativeUrl,
407
- projectDirectoryUrl,
408
- )
409
- const outDirectoryUrl = resolveDirectoryUrl(
410
- outDirectoryName,
411
- jsenvDirectoryUrl,
412
- )
413
- const outDirectoryRelativeUrl = urlToRelativeUrl(
414
- outDirectoryUrl,
415
- projectDirectoryUrl,
416
- )
417
-
418
- return outDirectoryRelativeUrl
419
- }
420
-
421
- const assertArguments = ({
422
- projectDirectoryUrl,
423
- jsenvDirectoryRelativeUrl,
424
- outDirectoryName,
425
- }) => {
426
- if (typeof projectDirectoryUrl !== "string") {
427
- throw new TypeError(
428
- `projectDirectoryUrl must be a string. got ${projectDirectoryUrl}`,
429
- )
430
- }
431
-
432
- if (typeof jsenvDirectoryRelativeUrl !== "string") {
433
- throw new TypeError(
434
- `jsenvDirectoryRelativeUrl must be a string. got ${jsenvDirectoryRelativeUrl}`,
435
- )
436
- }
437
- const jsenvDirectoryUrl = resolveDirectoryUrl(
438
- jsenvDirectoryRelativeUrl,
439
- projectDirectoryUrl,
440
- )
441
-
442
- if (!jsenvDirectoryUrl.startsWith(projectDirectoryUrl)) {
443
- throw new TypeError(
444
- createDetailedMessage(
445
- `jsenv directory must be inside project directory`,
446
- {
447
- ["jsenv directory url"]: jsenvDirectoryUrl,
448
- ["project directory url"]: projectDirectoryUrl,
449
- },
450
- ),
451
- )
452
- }
453
-
454
- if (typeof outDirectoryName !== "string") {
455
- throw new TypeError(
456
- `outDirectoryName must be a string. got ${outDirectoryName}`,
457
- )
458
- }
459
- }
460
-
461
- const setupOutDirectory = async ({
462
- logger,
463
- outDirectoryMeta,
464
- outDirectoryUrl,
465
- jsenvDirectoryClean,
466
- jsenvDirectoryUrl,
467
- }) => {
468
- if (jsenvDirectoryClean) {
469
- logger.debug(
470
- `Cleaning jsenv directory because jsenvDirectoryClean parameter enabled`,
471
- )
472
- await ensureEmptyDirectory(jsenvDirectoryUrl)
473
- }
474
- const metaFileUrl = resolveUrl("./meta.json", outDirectoryUrl)
475
-
476
- let previousOutDirectoryMeta
477
- try {
478
- const source = await readFile(metaFileUrl)
479
- previousOutDirectoryMeta = JSON.parse(source)
480
- } catch (e) {
481
- if (e && e.code === "ENOENT") {
482
- previousOutDirectoryMeta = null
483
- } else {
484
- throw e
485
- }
486
- }
487
-
488
- if (previousOutDirectoryMeta !== null) {
489
- const outDirectoryChanges = getOutDirectoryChanges(
490
- previousOutDirectoryMeta,
491
- outDirectoryMeta,
492
- )
493
-
494
- if (outDirectoryChanges) {
495
- if (!jsenvDirectoryClean) {
496
- logger.debug(
497
- createDetailedMessage(
498
- `Cleaning jsenv ${urlToBasename(
499
- outDirectoryUrl.slice(0, -1),
500
- )} directory because configuration has changed.`,
501
- {
502
- "changes": outDirectoryChanges.namedChanges
503
- ? outDirectoryChanges.namedChanges
504
- : `something`,
505
- "out directory": urlToFileSystemPath(outDirectoryUrl),
506
- },
507
- ),
508
- )
509
- }
510
- await ensureEmptyDirectory(outDirectoryUrl)
511
- }
512
- }
513
- }
514
-
515
- const getOutDirectoryChanges = (previousOutDirectoryMeta, outDirectoryMeta) => {
516
- const changes = []
517
-
518
- Object.keys(outDirectoryMeta).forEach((key) => {
519
- const now = outDirectoryMeta[key]
520
- const previous = previousOutDirectoryMeta[key]
521
- if (!compareValueJson(now, previous)) {
522
- changes.push(key)
523
- }
524
- })
525
-
526
- if (changes.length > 0) {
527
- return { namedChanges: changes }
528
- }
529
-
530
- // in case basic comparison from above is not enough
531
- if (!compareValueJson(previousOutDirectoryMeta, outDirectoryMeta)) {
532
- return { somethingChanged: true }
533
- }
534
-
535
- return null
536
- }
537
-
538
- const compareValueJson = (left, right) => {
539
- return JSON.stringify(left) === JSON.stringify(right)
540
- }
541
-
542
- // eslint-disable-next-line valid-jsdoc
543
- /**
544
- * We need to get two things:
545
- * { projectFileRequestedCallback, trackMainAndDependencies }
546
- *
547
- * projectFileRequestedCallback
548
- * This function will be called by the compile server every time a file inside projectDirectory
549
- * is requested so that we can build up the dependency tree of any file
550
- *
551
- * trackMainAndDependencies
552
- * This function is meant to be used to implement server sent events in order for a client to know
553
- * when a given file or any of its dependencies changes in order to implement livereloading.
554
- * At any time this function can be called with (mainRelativeUrl, { modified, removed, lastEventId })
555
- * modified is called
556
- * - immediatly if lastEventId is passed and mainRelativeUrl or any of its dependencies have
557
- * changed since that event (last change is passed to modified if their is more than one change)
558
- * - when mainRelativeUrl or any of its dependencies is modified
559
- * removed is called
560
- * - with same spec as modified but when a file is deleted from the filesystem instead of modified
561
- *
562
- */
563
- const setupServerSentEventsForLivereload = ({
564
- serverStopCallbackList,
565
- projectDirectoryUrl,
566
- jsenvDirectoryRelativeUrl,
567
- outDirectoryRelativeUrl,
568
- livereloadLogLevel,
569
- livereloadWatchConfig,
570
- }) => {
571
- const livereloadLogger = createLogger({ logLevel: livereloadLogLevel })
572
- const trackerMap = new Map()
573
- const projectFileRequested = createCallbackList()
574
- const projectFileModified = createCallbackList()
575
- const projectFileRemoved = createCallbackList()
576
- const projectFileAdded = createCallbackList()
577
-
578
- const projectFileRequestedCallback = (relativeUrl, request) => {
579
- // I doubt a compilation asset like .js.map will change
580
- // in theory a compilation asset should not change
581
- // if the source file did not change
582
- // so we can avoid watching compilation asset
583
- if (urlIsCompilationAsset(`${projectDirectoryUrl}${relativeUrl}`)) {
584
- return
585
- }
586
-
587
- projectFileRequested.notify({ relativeUrl, request })
588
- }
589
- const watchDescription = {
590
- ...livereloadWatchConfig,
591
- [jsenvDirectoryRelativeUrl]: false,
592
- }
593
- const unregisterDirectoryLifecyle = registerDirectoryLifecycle(
594
- projectDirectoryUrl,
595
- {
596
- watchDescription,
597
- updated: ({ relativeUrl }) => {
598
- projectFileModified.notify(relativeUrl)
599
- },
600
- removed: ({ relativeUrl }) => {
601
- projectFileRemoved.notify(relativeUrl)
602
- },
603
- added: ({ relativeUrl }) => {
604
- projectFileAdded.notify(relativeUrl)
605
- },
606
- keepProcessAlive: false,
607
- recursive: true,
608
- },
609
- )
610
- serverStopCallbackList.add(unregisterDirectoryLifecyle)
611
-
612
- const getDependencySet = (mainRelativeUrl) => {
613
- if (trackerMap.has(mainRelativeUrl)) {
614
- return trackerMap.get(mainRelativeUrl)
615
- }
616
- const dependencySet = new Set()
617
- dependencySet.add(mainRelativeUrl)
618
- trackerMap.set(mainRelativeUrl, dependencySet)
619
- return dependencySet
620
- }
621
-
622
- // each time a file is requested for the first time its dependencySet is computed
623
- projectFileRequested.add(({ relativeUrl: mainRelativeUrl }) => {
624
- // for now no use case of livereloading on node.js
625
- // and for browsers only html file can be main files
626
- // this avoid collecting dependencies of non html files that will never be used
627
- if (!mainRelativeUrl.endsWith(".html")) {
628
- return
629
- }
630
-
631
- // when a file is requested, always rebuild its dependency in case it has changed
632
- // since the last time it was requested
633
- const dependencySet = new Set()
634
- dependencySet.add(mainRelativeUrl)
635
- trackerMap.set(mainRelativeUrl, dependencySet)
636
-
637
- const removeDependencyRequestedCallback = projectFileRequested.add(
638
- ({ relativeUrl, request }) => {
639
- if (dependencySet.has(relativeUrl)) {
640
- return
641
- }
642
-
643
- const dependencyReport = reportDependency(
644
- relativeUrl,
645
- mainRelativeUrl,
646
- request,
647
- )
648
- if (dependencyReport.dependency === false) {
649
- livereloadLogger.debug(
650
- `${relativeUrl} not a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
651
- )
652
- return
653
- }
654
-
655
- livereloadLogger.debug(
656
- `${relativeUrl} is a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
657
- )
658
- dependencySet.add(relativeUrl)
659
- },
660
- )
661
- const removeMainRemovedCallback = projectFileRemoved.add((relativeUrl) => {
662
- if (relativeUrl === mainRelativeUrl) {
663
- removeDependencyRequestedCallback()
664
- removeMainRemovedCallback()
665
- trackerMap.delete(mainRelativeUrl)
666
- }
667
- })
668
- })
669
-
670
- const trackMainAndDependencies = (
671
- mainRelativeUrl,
672
- { modified, removed, added },
673
- ) => {
674
- livereloadLogger.debug(`track ${mainRelativeUrl} and its dependencies`)
675
-
676
- const removeModifiedCallback = projectFileModified.add((relativeUrl) => {
677
- const dependencySet = getDependencySet(mainRelativeUrl)
678
- if (dependencySet.has(relativeUrl)) {
679
- modified(relativeUrl)
680
- }
681
- })
682
- const removeRemovedCallback = projectFileRemoved.add((relativeUrl) => {
683
- const dependencySet = getDependencySet(mainRelativeUrl)
684
- if (dependencySet.has(relativeUrl)) {
685
- removed(relativeUrl)
686
- }
687
- })
688
- const removeAddedCallback = projectFileAdded.add((relativeUrl) => {
689
- const dependencySet = getDependencySet(mainRelativeUrl)
690
- if (dependencySet.has(relativeUrl)) {
691
- added(relativeUrl)
692
- }
693
- })
694
-
695
- return () => {
696
- livereloadLogger.debug(
697
- `stop tracking ${mainRelativeUrl} and its dependencies.`,
698
- )
699
- removeModifiedCallback()
700
- removeRemovedCallback()
701
- removeAddedCallback()
702
- }
703
- }
704
-
705
- const reportDependency = (relativeUrl, mainRelativeUrl, request) => {
706
- if (relativeUrl === mainRelativeUrl) {
707
- return {
708
- dependency: true,
709
- reason: "it's main",
710
- }
711
- }
712
-
713
- if ("x-jsenv-execution-id" in request.headers) {
714
- const executionId = request.headers["x-jsenv-execution-id"]
715
- if (executionId === mainRelativeUrl) {
716
- return {
717
- dependency: true,
718
- reason: "x-jsenv-execution-id request header",
719
- }
720
- }
721
- return {
722
- dependency: false,
723
- reason: "x-jsenv-execution-id request header",
724
- }
725
- }
726
-
727
- const { referer } = request.headers
728
- if (referer) {
729
- const { origin } = request
730
- // referer is likely the exploringServer
731
- if (referer !== origin && !urlIsInsideOf(referer, origin)) {
732
- return {
733
- dependency: false,
734
- reason: "referer is an other origin",
735
- }
736
- }
737
- // here we know the referer is inside compileServer
738
- const refererRelativeUrl = urlToOriginalRelativeUrl(
739
- referer,
740
- resolveUrl(outDirectoryRelativeUrl, request.origin),
741
- )
742
- if (refererRelativeUrl) {
743
- // search if referer (file requesting this one) is tracked as being a dependency of main file
744
- // in that case because the importer is a dependency the importee is also a dependency
745
- // eslint-disable-next-line no-unused-vars
746
- for (const tracker of trackerMap) {
747
- if (
748
- tracker[0] === mainRelativeUrl &&
749
- tracker[1].has(refererRelativeUrl)
750
- ) {
751
- return {
752
- dependency: true,
753
- reason: "referer is a dependency",
754
- }
755
- }
756
- }
757
- }
758
- }
759
-
760
- return {
761
- dependency: true,
762
- reason: "it was requested",
763
- }
764
- }
765
-
766
- return {
767
- projectFileRequestedCallback,
768
- trackMainAndDependencies,
769
- }
770
- }
771
-
772
- const createSSEForLivereloadService = ({
773
- serverStopCallbackList,
774
- outDirectoryRelativeUrl,
775
- trackMainAndDependencies,
776
- }) => {
777
- const cache = []
778
- const sseRoomLimit = 100
779
- const getOrCreateSSERoom = (mainFileRelativeUrl) => {
780
- const cacheEntry = cache.find(
781
- (cacheEntryCandidate) =>
782
- cacheEntryCandidate.mainFileRelativeUrl === mainFileRelativeUrl,
783
- )
784
- if (cacheEntry) {
785
- return cacheEntry.sseRoom
786
- }
787
-
788
- const sseRoom = createSSERoom({
789
- retryDuration: 2000,
790
- historyLength: 100,
791
- welcomeEventEnabled: true,
792
- })
793
-
794
- // each time something is modified or removed we send event to the room
795
- const stopTracking = trackMainAndDependencies(mainFileRelativeUrl, {
796
- modified: (relativeUrl) => {
797
- sseRoom.sendEvent({ type: "file-modified", data: relativeUrl })
798
- },
799
- removed: (relativeUrl) => {
800
- sseRoom.sendEvent({ type: "file-removed", data: relativeUrl })
801
- },
802
- added: (relativeUrl) => {
803
- sseRoom.sendEvent({ type: "file-added", data: relativeUrl })
804
- },
805
- })
806
-
807
- const removeSSECleanupCallback = serverStopCallbackList.add(() => {
808
- removeSSECleanupCallback()
809
- sseRoom.close()
810
- stopTracking()
811
- })
812
- cache.push({
813
- mainFileRelativeUrl,
814
- sseRoom,
815
- cleanup: () => {
816
- removeSSECleanupCallback()
817
- sseRoom.close()
818
- stopTracking()
819
- },
820
- })
821
- if (cache.length >= sseRoomLimit) {
822
- const firstCacheEntry = cache.shift()
823
- firstCacheEntry.cleanup()
824
- }
825
- return sseRoom
826
- }
827
-
828
- return (request) => {
829
- const { accept } = request.headers
830
- if (!accept || !accept.includes("text/event-stream")) {
831
- return null
832
- }
833
-
834
- const fileRelativeUrl = urlToOriginalRelativeUrl(
835
- resolveUrl(request.ressource, request.origin),
836
- resolveUrl(outDirectoryRelativeUrl, request.origin),
837
- )
838
-
839
- const room = getOrCreateSSERoom(fileRelativeUrl)
840
- return room.join(request)
841
- }
842
- }
843
-
844
- const urlToOriginalRelativeUrl = (url, outDirectoryRemoteUrl) => {
845
- if (urlIsInsideOf(url, outDirectoryRemoteUrl)) {
846
- const afterCompileDirectory = urlToRelativeUrl(url, outDirectoryRemoteUrl)
847
- const fileRelativeUrl = afterCompileDirectory.slice(
848
- afterCompileDirectory.indexOf("/") + 1,
849
- )
850
- return fileRelativeUrl
851
- }
852
- return new URL(url).pathname.slice(1)
853
- }
854
-
855
- const createCompilationAssetFileService = ({ projectDirectoryUrl }) => {
856
- return (request) => {
857
- const { origin, ressource } = request
858
- const requestUrl = `${origin}${ressource}`
859
- if (urlIsCompilationAsset(requestUrl)) {
860
- return fetchFileSystem(
861
- new URL(request.ressource.slice(1), projectDirectoryUrl),
862
- {
863
- headers: request.headers,
864
- etagEnabled: true,
865
- },
866
- )
867
- }
868
- return null
869
- }
870
- }
871
-
872
- const createBrowserScriptService = ({
873
- projectDirectoryUrl,
874
- outDirectoryRelativeUrl,
875
- }) => {
876
- const sourcemapMainFileRelativeUrl = urlToRelativeUrl(
877
- sourcemapMainFileInfo.url,
878
- projectDirectoryUrl,
879
- )
880
- const sourcemapMappingFileRelativeUrl = urlToRelativeUrl(
881
- sourcemapMappingFileInfo.url,
882
- projectDirectoryUrl,
883
- )
884
-
885
- return (request) => {
886
- if (
887
- request.method === "GET" &&
888
- request.ressource === "/.jsenv/compile-meta.json"
889
- ) {
890
- const body = JSON.stringify({
891
- outDirectoryRelativeUrl,
892
- errorStackRemapping: true,
893
- sourcemapMainFileRelativeUrl,
894
- sourcemapMappingFileRelativeUrl,
895
- })
896
-
897
- return {
898
- status: 200,
899
- headers: {
900
- "cache-control": "no-store",
901
- "content-type": "application/json",
902
- "content-length": Buffer.byteLength(body),
903
- },
904
- body,
905
- }
906
- }
907
-
908
- return null
909
- }
910
- }
911
-
912
- const createSourceFileService = ({
913
- projectDirectoryUrl,
914
- projectFileRequestedCallback,
915
- projectFileEtagEnabled,
916
- }) => {
917
- return async (request) => {
918
- const { ressource } = request
919
- const relativeUrl = ressource.slice(1)
920
- projectFileRequestedCallback(relativeUrl, request)
921
-
922
- const responsePromise = fetchFileSystem(
923
- new URL(request.ressource.slice(1), projectDirectoryUrl),
924
- {
925
- headers: request.headers,
926
- etagEnabled: projectFileEtagEnabled,
927
- },
928
- )
929
-
930
- return responsePromise
931
- }
932
- }
933
-
934
- const createOutJSONFiles = ({
935
- projectDirectoryUrl,
936
- jsenvDirectoryRelativeUrl,
937
- outDirectoryRelativeUrl,
938
- importDefaultExtension,
939
- compileServerGroupMap,
940
- babelPluginMap,
941
- replaceProcessEnvNodeEnv,
942
- processEnvNodeEnv,
943
- env,
944
- inlineImportMapIntoHTML,
945
- customCompilers,
946
- jsenvToolbarInjection,
947
- sourcemapMethod,
948
- sourcemapExcludeSources,
949
- }) => {
950
- const outJSONFiles = {}
951
- const outDirectoryUrl = resolveUrl(
952
- outDirectoryRelativeUrl,
953
- projectDirectoryUrl,
954
- )
955
-
956
- const metaOutFileUrl = resolveUrl("./meta.json", outDirectoryUrl)
957
- const jsenvCorePackageFileUrl = resolveUrl(
958
- "./package.json",
959
- jsenvCoreDirectoryUrl,
960
- )
961
- const jsenvCorePackageFilePath = urlToFileSystemPath(jsenvCorePackageFileUrl)
962
- const jsenvCorePackageVersion = readPackage(jsenvCorePackageFilePath).version
963
- const customCompilerPatterns = Object.keys(customCompilers)
964
- const outDirectoryMeta = {
965
- jsenvCorePackageVersion,
966
- babelPluginMap: babelPluginMapAsData(babelPluginMap),
967
- compileServerGroupMap,
968
- replaceProcessEnvNodeEnv,
969
- processEnvNodeEnv,
970
- customCompilerPatterns,
971
- jsenvToolbarInjection,
972
- sourcemapMethod,
973
- sourcemapExcludeSources,
974
- }
975
- outJSONFiles.meta = {
976
- url: metaOutFileUrl,
977
- data: outDirectoryMeta,
978
- }
979
-
980
- const envOutFileUrl = resolveUrl("./env.json", outDirectoryUrl)
981
- env = {
982
- ...env,
983
- jsenvDirectoryRelativeUrl,
984
- outDirectoryRelativeUrl,
985
- importDefaultExtension,
986
- inlineImportMapIntoHTML,
987
- customCompilerPatterns,
988
- }
989
- outJSONFiles.env = {
990
- url: envOutFileUrl,
991
- data: env,
992
- }
993
-
994
- const groupMapOutFileUrl = resolveUrl("./groupMap.json", outDirectoryUrl)
995
- outJSONFiles.groupMap = {
996
- url: groupMapOutFileUrl,
997
- data: compileServerGroupMap,
998
- }
999
-
1000
- return outJSONFiles
1001
- }
1002
-
1003
- const babelPluginMapAsData = (babelPluginMap) => {
1004
- const data = {}
1005
- Object.keys(babelPluginMap).forEach((key) => {
1006
- const value = babelPluginMap[key]
1007
- if (Array.isArray(value)) {
1008
- data[key] = value
1009
- return
1010
- }
1011
- if (typeof value === "object") {
1012
- data[key] = {
1013
- options: value.options,
1014
- }
1015
- return
1016
- }
1017
- data[key] = value
1018
- })
1019
- return data
1020
- }
1021
-
1022
- const createOutFilesService = async ({
1023
- logger,
1024
- projectDirectoryUrl,
1025
- compileServerCanWriteOnFilesystem,
1026
- outDirectoryUrl,
1027
- outJSONFiles,
1028
- }) => {
1029
- const isOutRootFile = (url) => {
1030
- if (!urlIsInsideOf(url, outDirectoryUrl)) {
1031
- return false
1032
- }
1033
- const afterOutDirectory = url.slice(outDirectoryUrl.length)
1034
- if (afterOutDirectory.indexOf("/") > -1) {
1035
- return false
1036
- }
1037
- return true
1038
- }
1039
-
1040
- if (compileServerCanWriteOnFilesystem) {
1041
- await Promise.all(
1042
- Object.keys(outJSONFiles).map(async (name) => {
1043
- const outJSONFile = outJSONFiles[name]
1044
- await writeFile(
1045
- outJSONFile.url,
1046
- JSON.stringify(outJSONFile.data, null, " "),
1047
- )
1048
- logger.debug(`-> ${outJSONFile.url}`)
1049
- }),
1050
- )
1051
-
1052
- return async (request) => {
1053
- const requestUrl = resolveUrl(
1054
- request.ressource.slice(1),
1055
- projectDirectoryUrl,
1056
- )
1057
- if (!isOutRootFile(requestUrl)) {
1058
- return null
1059
- }
1060
- return fetchFileSystem(
1061
- new URL(request.ressource.slice(1), projectDirectoryUrl),
1062
- {
1063
- headers: request.headers,
1064
- etagEnabled: true,
1065
- },
1066
- )
1067
- }
1068
- }
1069
- // serve from memory
1070
- return (request) => {
1071
- const requestUrl = resolveUrl(
1072
- request.ressource.slice(1),
1073
- projectDirectoryUrl,
1074
- )
1075
- if (!isOutRootFile(requestUrl)) {
1076
- return null
1077
- }
1078
-
1079
- const outJSONFileKey = Object.keys(outJSONFiles).find((name) => {
1080
- return outJSONFiles[name].url === requestUrl
1081
- })
1082
- if (!outJSONFileKey) {
1083
- return {
1084
- status: 404,
1085
- }
1086
- }
1087
-
1088
- const outJSONFile = outJSONFiles[outJSONFileKey]
1089
- const body = JSON.stringify(outJSONFile.data, null, " ")
1090
- return {
1091
- status: 200,
1092
- headers: {
1093
- "content-type": urlToContentType(requestUrl),
1094
- "content-length": Buffer.byteLength(body),
1095
- },
1096
- body,
1097
- }
1098
- }
1099
- }
1100
-
1101
- const createCompileProxyService = ({ projectDirectoryUrl }) => {
1102
- const jsenvCompileProxyRelativeUrlForProject = urlToRelativeUrl(
1103
- jsenvCompileProxyFileInfo.jsenvBuildUrl,
1104
- projectDirectoryUrl,
1105
- )
1106
-
1107
- return (request) => {
1108
- if (request.ressource === "/.jsenv/jsenv_compile_proxy.js") {
1109
- const jsenvCompileProxyBuildServerUrl = `${request.origin}/${jsenvCompileProxyRelativeUrlForProject}`
1110
- return {
1111
- status: 307,
1112
- headers: {
1113
- location: jsenvCompileProxyBuildServerUrl,
1114
- },
1115
- }
1116
- }
1117
-
1118
- return null
1119
- }
1120
- }
1121
-
1122
- const readPackage = (packagePath) => {
1123
- const buffer = readFileSync(packagePath)
1124
- const string = String(buffer)
1125
- const packageObject = JSON.parse(string)
1126
- return packageObject
1127
- }
1128
-
1129
- const __filenameReplacement = `import.meta.url.slice('file:///'.length)`
1130
-
1131
- 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
+ } 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 = "etag",
67
+ projectFileEtagEnabled = true,
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
+ logger,
338
+ projectDirectoryUrl,
339
+ projectFileRequestedCallback,
340
+ projectFileEtagEnabled,
341
+ transformHtmlSourceFiles,
342
+ inlineImportMapIntoHTML,
343
+ jsenvScriptInjection,
344
+ jsenvToolbarInjection,
345
+ }),
346
+ }
347
+
348
+ const compileServer = await startServer({
349
+ signal,
350
+ stopOnExit: false,
351
+ stopOnSIGINT: handleSIGINT,
352
+ stopOnInternalError: false,
353
+ sendServerInternalErrorDetails: true,
354
+ keepProcessAlive,
355
+
356
+ logLevel: compileServerLogLevel,
357
+
358
+ protocol: compileServerProtocol,
359
+ http2: compileServerHttp2,
360
+ serverCertificate: compileServerCertificate,
361
+ serverCertificatePrivateKey: compileServerPrivateKey,
362
+ ip: compileServerIp,
363
+ port: compileServerPort,
364
+ plugins: {
365
+ ...pluginCORS({
366
+ accessControlAllowRequestOrigin: true,
367
+ accessControlAllowRequestMethod: true,
368
+ accessControlAllowRequestHeaders: true,
369
+ accessControlAllowedRequestHeaders: [
370
+ ...jsenvAccessControlAllowedHeaders,
371
+ "x-jsenv-execution-id",
372
+ ],
373
+ accessControlAllowCredentials: true,
374
+ }),
375
+ ...pluginServerTiming,
376
+ ...pluginRequestWaitingCheck({
377
+ requestWaitingMs: 60 * 1000,
378
+ }),
379
+ },
380
+ requestToResponse: composeServicesWithTiming({
381
+ ...customServices,
382
+ ...jsenvServices,
383
+ }),
384
+ onStop: (reason) => {
385
+ onStop()
386
+ serverStopCallbackList.notify(reason)
387
+ },
388
+ })
389
+
390
+ return {
391
+ jsenvDirectoryRelativeUrl,
392
+ outDirectoryRelativeUrl,
393
+ ...compileServer,
394
+ compileServerGroupMap,
395
+ babelPluginMap,
396
+ }
397
+ }
398
+
399
+ export const computeOutDirectoryRelativeUrl = ({
400
+ projectDirectoryUrl,
401
+ jsenvDirectoryRelativeUrl = ".jsenv",
402
+ outDirectoryName = "out",
403
+ }) => {
404
+ const jsenvDirectoryUrl = resolveDirectoryUrl(
405
+ jsenvDirectoryRelativeUrl,
406
+ projectDirectoryUrl,
407
+ )
408
+ const outDirectoryUrl = resolveDirectoryUrl(
409
+ outDirectoryName,
410
+ jsenvDirectoryUrl,
411
+ )
412
+ const outDirectoryRelativeUrl = urlToRelativeUrl(
413
+ outDirectoryUrl,
414
+ projectDirectoryUrl,
415
+ )
416
+
417
+ return outDirectoryRelativeUrl
418
+ }
419
+
420
+ const assertArguments = ({
421
+ projectDirectoryUrl,
422
+ jsenvDirectoryRelativeUrl,
423
+ outDirectoryName,
424
+ }) => {
425
+ if (typeof projectDirectoryUrl !== "string") {
426
+ throw new TypeError(
427
+ `projectDirectoryUrl must be a string. got ${projectDirectoryUrl}`,
428
+ )
429
+ }
430
+
431
+ if (typeof jsenvDirectoryRelativeUrl !== "string") {
432
+ throw new TypeError(
433
+ `jsenvDirectoryRelativeUrl must be a string. got ${jsenvDirectoryRelativeUrl}`,
434
+ )
435
+ }
436
+ const jsenvDirectoryUrl = resolveDirectoryUrl(
437
+ jsenvDirectoryRelativeUrl,
438
+ projectDirectoryUrl,
439
+ )
440
+
441
+ if (!jsenvDirectoryUrl.startsWith(projectDirectoryUrl)) {
442
+ throw new TypeError(
443
+ createDetailedMessage(
444
+ `jsenv directory must be inside project directory`,
445
+ {
446
+ ["jsenv directory url"]: jsenvDirectoryUrl,
447
+ ["project directory url"]: projectDirectoryUrl,
448
+ },
449
+ ),
450
+ )
451
+ }
452
+
453
+ if (typeof outDirectoryName !== "string") {
454
+ throw new TypeError(
455
+ `outDirectoryName must be a string. got ${outDirectoryName}`,
456
+ )
457
+ }
458
+ }
459
+
460
+ const cleanOutDirectoryIfNeeded = async ({
461
+ logger,
462
+ outDirectoryUrl,
463
+ jsenvDirectoryClean,
464
+ jsenvDirectoryUrl,
465
+ compileServerMetaFileInfo,
466
+ }) => {
467
+ if (jsenvDirectoryClean) {
468
+ logger.debug(
469
+ `Cleaning jsenv directory because jsenvDirectoryClean parameter enabled`,
470
+ )
471
+ await ensureEmptyDirectory(jsenvDirectoryUrl)
472
+ }
473
+
474
+ let previousCompileServerMeta
475
+ try {
476
+ const source = await readFile(compileServerMetaFileInfo.url)
477
+ previousCompileServerMeta = JSON.parse(source)
478
+ } catch (e) {
479
+ if (e && e.code === "ENOENT") {
480
+ previousCompileServerMeta = null
481
+ } else {
482
+ throw e
483
+ }
484
+ }
485
+
486
+ if (previousCompileServerMeta !== null) {
487
+ const outDirectoryChanges = getOutDirectoryChanges(
488
+ previousCompileServerMeta,
489
+ compileServerMetaFileInfo.data,
490
+ )
491
+
492
+ if (outDirectoryChanges) {
493
+ if (!jsenvDirectoryClean) {
494
+ logger.debug(
495
+ createDetailedMessage(
496
+ `Cleaning jsenv ${urlToBasename(
497
+ outDirectoryUrl.slice(0, -1),
498
+ )} directory because configuration has changed.`,
499
+ {
500
+ "changes": outDirectoryChanges.namedChanges
501
+ ? outDirectoryChanges.namedChanges
502
+ : `something`,
503
+ "out directory": urlToFileSystemPath(outDirectoryUrl),
504
+ },
505
+ ),
506
+ )
507
+ }
508
+ await ensureEmptyDirectory(outDirectoryUrl)
509
+ }
510
+ }
511
+ }
512
+
513
+ const getOutDirectoryChanges = (
514
+ previousCompileServerMeta,
515
+ compileServerMeta,
516
+ ) => {
517
+ const changes = []
518
+
519
+ Object.keys(compileServerMeta).forEach((key) => {
520
+ const now = compileServerMeta[key]
521
+ const previous = previousCompileServerMeta[key]
522
+ if (!compareValueJson(now, previous)) {
523
+ changes.push(key)
524
+ }
525
+ })
526
+
527
+ if (changes.length > 0) {
528
+ return { namedChanges: changes }
529
+ }
530
+
531
+ // in case basic comparison from above is not enough
532
+ if (!compareValueJson(previousCompileServerMeta, compileServerMeta)) {
533
+ return { somethingChanged: true }
534
+ }
535
+
536
+ return null
537
+ }
538
+
539
+ const compareValueJson = (left, right) => {
540
+ return JSON.stringify(left) === JSON.stringify(right)
541
+ }
542
+
543
+ /**
544
+ * We need to get two things:
545
+ * { projectFileRequestedCallback, trackMainAndDependencies }
546
+ *
547
+ * projectFileRequestedCallback
548
+ * This function will be called by the compile server every time a file inside projectDirectory
549
+ * is requested so that we can build up the dependency tree of any file
550
+ *
551
+ * trackMainAndDependencies
552
+ * This function is meant to be used to implement server sent events in order for a client to know
553
+ * when a given file or any of its dependencies changes in order to implement livereloading.
554
+ * At any time this function can be called with (mainRelativeUrl, { modified, removed, lastEventId })
555
+ * modified is called
556
+ * - immediatly if lastEventId is passed and mainRelativeUrl or any of its dependencies have
557
+ * changed since that event (last change is passed to modified if their is more than one change)
558
+ * - when mainRelativeUrl or any of its dependencies is modified
559
+ * removed is called
560
+ * - with same spec as modified but when a file is deleted from the filesystem instead of modified
561
+ *
562
+ */
563
+ const setupServerSentEventsForLivereload = ({
564
+ serverStopCallbackList,
565
+ projectDirectoryUrl,
566
+ jsenvDirectoryRelativeUrl,
567
+ outDirectoryRelativeUrl,
568
+ livereloadLogLevel,
569
+ livereloadWatchConfig,
570
+ }) => {
571
+ const livereloadLogger = createLogger({ logLevel: livereloadLogLevel })
572
+ const trackerMap = new Map()
573
+ const projectFileRequested = createCallbackList()
574
+ const projectFileModified = createCallbackList()
575
+ const projectFileRemoved = createCallbackList()
576
+ const projectFileAdded = createCallbackList()
577
+
578
+ const projectFileRequestedCallback = (relativeUrl, request) => {
579
+ // I doubt a compilation asset like .js.map will change
580
+ // in theory a compilation asset should not change
581
+ // if the source file did not change
582
+ // so we can avoid watching compilation asset
583
+ if (urlIsCompilationAsset(`${projectDirectoryUrl}${relativeUrl}`)) {
584
+ return
585
+ }
586
+
587
+ projectFileRequested.notify({ relativeUrl, request })
588
+ }
589
+ const watchDescription = {
590
+ ...livereloadWatchConfig,
591
+ [jsenvDirectoryRelativeUrl]: false,
592
+ }
593
+ const unregisterDirectoryLifecyle = registerDirectoryLifecycle(
594
+ projectDirectoryUrl,
595
+ {
596
+ watchDescription,
597
+ updated: ({ relativeUrl }) => {
598
+ projectFileModified.notify(relativeUrl)
599
+ },
600
+ removed: ({ relativeUrl }) => {
601
+ projectFileRemoved.notify(relativeUrl)
602
+ },
603
+ added: ({ relativeUrl }) => {
604
+ projectFileAdded.notify(relativeUrl)
605
+ },
606
+ keepProcessAlive: false,
607
+ recursive: true,
608
+ },
609
+ )
610
+ serverStopCallbackList.add(unregisterDirectoryLifecyle)
611
+
612
+ const getDependencySet = (mainRelativeUrl) => {
613
+ if (trackerMap.has(mainRelativeUrl)) {
614
+ return trackerMap.get(mainRelativeUrl)
615
+ }
616
+ const dependencySet = new Set()
617
+ dependencySet.add(mainRelativeUrl)
618
+ trackerMap.set(mainRelativeUrl, dependencySet)
619
+ return dependencySet
620
+ }
621
+
622
+ // each time a file is requested for the first time its dependencySet is computed
623
+ projectFileRequested.add(({ relativeUrl: mainRelativeUrl }) => {
624
+ // for now no use case of livereloading on node.js
625
+ // and for browsers only html file can be main files
626
+ // this avoid collecting dependencies of non html files that will never be used
627
+ if (!mainRelativeUrl.endsWith(".html")) {
628
+ return
629
+ }
630
+
631
+ // when a file is requested, always rebuild its dependency in case it has changed
632
+ // since the last time it was requested
633
+ const dependencySet = new Set()
634
+ dependencySet.add(mainRelativeUrl)
635
+ trackerMap.set(mainRelativeUrl, dependencySet)
636
+
637
+ const removeDependencyRequestedCallback = projectFileRequested.add(
638
+ ({ relativeUrl, request }) => {
639
+ if (dependencySet.has(relativeUrl)) {
640
+ return
641
+ }
642
+
643
+ const dependencyReport = reportDependency(
644
+ relativeUrl,
645
+ mainRelativeUrl,
646
+ request,
647
+ )
648
+ if (dependencyReport.dependency === false) {
649
+ livereloadLogger.debug(
650
+ `${relativeUrl} not a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
651
+ )
652
+ return
653
+ }
654
+
655
+ livereloadLogger.debug(
656
+ `${relativeUrl} is a dependency of ${mainRelativeUrl} because ${dependencyReport.reason}`,
657
+ )
658
+ dependencySet.add(relativeUrl)
659
+ },
660
+ )
661
+ const removeMainRemovedCallback = projectFileRemoved.add((relativeUrl) => {
662
+ if (relativeUrl === mainRelativeUrl) {
663
+ removeDependencyRequestedCallback()
664
+ removeMainRemovedCallback()
665
+ trackerMap.delete(mainRelativeUrl)
666
+ }
667
+ })
668
+ })
669
+
670
+ const trackMainAndDependencies = (
671
+ mainRelativeUrl,
672
+ { modified, removed, added },
673
+ ) => {
674
+ livereloadLogger.debug(`track ${mainRelativeUrl} and its dependencies`)
675
+
676
+ const removeModifiedCallback = projectFileModified.add((relativeUrl) => {
677
+ const dependencySet = getDependencySet(mainRelativeUrl)
678
+ if (dependencySet.has(relativeUrl)) {
679
+ modified(relativeUrl)
680
+ }
681
+ })
682
+ const removeRemovedCallback = projectFileRemoved.add((relativeUrl) => {
683
+ const dependencySet = getDependencySet(mainRelativeUrl)
684
+ if (dependencySet.has(relativeUrl)) {
685
+ removed(relativeUrl)
686
+ }
687
+ })
688
+ const removeAddedCallback = projectFileAdded.add((relativeUrl) => {
689
+ const dependencySet = getDependencySet(mainRelativeUrl)
690
+ if (dependencySet.has(relativeUrl)) {
691
+ added(relativeUrl)
692
+ }
693
+ })
694
+
695
+ return () => {
696
+ livereloadLogger.debug(
697
+ `stop tracking ${mainRelativeUrl} and its dependencies.`,
698
+ )
699
+ removeModifiedCallback()
700
+ removeRemovedCallback()
701
+ removeAddedCallback()
702
+ }
703
+ }
704
+
705
+ const reportDependency = (relativeUrl, mainRelativeUrl, request) => {
706
+ if (relativeUrl === mainRelativeUrl) {
707
+ return {
708
+ dependency: true,
709
+ reason: "it's main",
710
+ }
711
+ }
712
+
713
+ if ("x-jsenv-execution-id" in request.headers) {
714
+ const executionId = request.headers["x-jsenv-execution-id"]
715
+ if (executionId === mainRelativeUrl) {
716
+ return {
717
+ dependency: true,
718
+ reason: "x-jsenv-execution-id request header",
719
+ }
720
+ }
721
+ return {
722
+ dependency: false,
723
+ reason: "x-jsenv-execution-id request header",
724
+ }
725
+ }
726
+
727
+ const { referer } = request.headers
728
+ if (referer) {
729
+ const { origin } = request
730
+ // referer is likely the exploringServer
731
+ if (referer !== origin && !urlIsInsideOf(referer, origin)) {
732
+ return {
733
+ dependency: false,
734
+ reason: "referer is an other origin",
735
+ }
736
+ }
737
+ // here we know the referer is inside compileServer
738
+ const refererRelativeUrl = urlToOriginalRelativeUrl(
739
+ referer,
740
+ resolveUrl(outDirectoryRelativeUrl, request.origin),
741
+ )
742
+ if (refererRelativeUrl) {
743
+ // search if referer (file requesting this one) is tracked as being a dependency of main file
744
+ // in that case because the importer is a dependency the importee is also a dependency
745
+ // eslint-disable-next-line no-unused-vars
746
+ for (const tracker of trackerMap) {
747
+ if (
748
+ tracker[0] === mainRelativeUrl &&
749
+ tracker[1].has(refererRelativeUrl)
750
+ ) {
751
+ return {
752
+ dependency: true,
753
+ reason: "referer is a dependency",
754
+ }
755
+ }
756
+ }
757
+ }
758
+ }
759
+
760
+ return {
761
+ dependency: true,
762
+ reason: "it was requested",
763
+ }
764
+ }
765
+
766
+ return {
767
+ projectFileRequestedCallback,
768
+ trackMainAndDependencies,
769
+ }
770
+ }
771
+
772
+ const createSSEForLivereloadService = ({
773
+ serverStopCallbackList,
774
+ outDirectoryRelativeUrl,
775
+ trackMainAndDependencies,
776
+ }) => {
777
+ const cache = []
778
+ const sseRoomLimit = 100
779
+ const getOrCreateSSERoom = (mainFileRelativeUrl) => {
780
+ const cacheEntry = cache.find(
781
+ (cacheEntryCandidate) =>
782
+ cacheEntryCandidate.mainFileRelativeUrl === mainFileRelativeUrl,
783
+ )
784
+ if (cacheEntry) {
785
+ return cacheEntry.sseRoom
786
+ }
787
+
788
+ const sseRoom = createSSERoom({
789
+ retryDuration: 2000,
790
+ historyLength: 100,
791
+ welcomeEventEnabled: true,
792
+ })
793
+
794
+ // each time something is modified or removed we send event to the room
795
+ const stopTracking = trackMainAndDependencies(mainFileRelativeUrl, {
796
+ modified: (relativeUrl) => {
797
+ sseRoom.sendEvent({ type: "file-modified", data: relativeUrl })
798
+ },
799
+ removed: (relativeUrl) => {
800
+ sseRoom.sendEvent({ type: "file-removed", data: relativeUrl })
801
+ },
802
+ added: (relativeUrl) => {
803
+ sseRoom.sendEvent({ type: "file-added", data: relativeUrl })
804
+ },
805
+ })
806
+
807
+ const removeSSECleanupCallback = serverStopCallbackList.add(() => {
808
+ removeSSECleanupCallback()
809
+ sseRoom.close()
810
+ stopTracking()
811
+ })
812
+ cache.push({
813
+ mainFileRelativeUrl,
814
+ sseRoom,
815
+ cleanup: () => {
816
+ removeSSECleanupCallback()
817
+ sseRoom.close()
818
+ stopTracking()
819
+ },
820
+ })
821
+ if (cache.length >= sseRoomLimit) {
822
+ const firstCacheEntry = cache.shift()
823
+ firstCacheEntry.cleanup()
824
+ }
825
+ return sseRoom
826
+ }
827
+
828
+ return (request) => {
829
+ const { accept } = request.headers
830
+ if (!accept || !accept.includes("text/event-stream")) {
831
+ return null
832
+ }
833
+
834
+ const fileRelativeUrl = urlToOriginalRelativeUrl(
835
+ resolveUrl(request.ressource, request.origin),
836
+ resolveUrl(outDirectoryRelativeUrl, request.origin),
837
+ )
838
+
839
+ const room = getOrCreateSSERoom(fileRelativeUrl)
840
+ return room.join(request)
841
+ }
842
+ }
843
+
844
+ const urlToOriginalRelativeUrl = (url, outDirectoryRemoteUrl) => {
845
+ if (urlIsInsideOf(url, outDirectoryRemoteUrl)) {
846
+ const afterCompileDirectory = urlToRelativeUrl(url, outDirectoryRemoteUrl)
847
+ const fileRelativeUrl = afterCompileDirectory.slice(
848
+ afterCompileDirectory.indexOf("/") + 1,
849
+ )
850
+ return fileRelativeUrl
851
+ }
852
+ return new URL(url).pathname.slice(1)
853
+ }
854
+
855
+ const createCompilationAssetFileService = ({ projectDirectoryUrl }) => {
856
+ return (request) => {
857
+ const { origin, ressource } = request
858
+ const requestUrl = `${origin}${ressource}`
859
+ if (urlIsCompilationAsset(requestUrl)) {
860
+ return fetchFileSystem(
861
+ new URL(request.ressource.slice(1), projectDirectoryUrl),
862
+ {
863
+ headers: request.headers,
864
+ etagEnabled: true,
865
+ },
866
+ )
867
+ }
868
+ return null
869
+ }
870
+ }
871
+
872
+ const createSourceFileService = ({
873
+ projectDirectoryUrl,
874
+ projectFileRequestedCallback,
875
+ projectFileEtagEnabled,
876
+ }) => {
877
+ return async (request) => {
878
+ const { ressource } = request
879
+ const relativeUrl = ressource.slice(1)
880
+ projectFileRequestedCallback(relativeUrl, request)
881
+
882
+ const responsePromise = fetchFileSystem(
883
+ new URL(request.ressource.slice(1), projectDirectoryUrl),
884
+ {
885
+ headers: request.headers,
886
+ etagEnabled: projectFileEtagEnabled,
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(/[\\\/\\\\][^\\\/\\\\]*$/, '')`