@jsenv/core 33.0.2 → 34.0.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 (29) hide show
  1. package/dist/js/autoreload.js +1 -4
  2. package/dist/js/supervisor.js +498 -290
  3. package/dist/jsenv.js +870 -346
  4. package/package.json +2 -3
  5. package/src/basic_fetch.js +23 -13
  6. package/src/build/start_build_server.js +3 -2
  7. package/src/dev/file_service.js +1 -1
  8. package/src/dev/start_dev_server.js +9 -6
  9. package/src/execute/execute.js +7 -18
  10. package/src/execute/runtimes/browsers/from_playwright.js +168 -32
  11. package/src/execute/runtimes/browsers/webkit.js +1 -1
  12. package/src/execute/web_server_param.js +68 -0
  13. package/src/kitchen/compat/features_compatibility.js +3 -0
  14. package/src/plugins/autoreload/client/reload.js +1 -4
  15. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +30 -18
  16. package/src/plugins/plugins.js +1 -1
  17. package/src/plugins/ribbon/jsenv_plugin_ribbon.js +3 -2
  18. package/src/plugins/supervisor/client/supervisor.js +467 -287
  19. package/src/plugins/supervisor/html_supervisor_injection.js +281 -0
  20. package/src/plugins/supervisor/js_supervisor_injection.js +281 -0
  21. package/src/plugins/supervisor/jsenv_plugin_supervisor.js +48 -233
  22. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +1 -1
  23. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +5 -0
  24. package/src/plugins/transpilation/jsenv_plugin_top_level_await.js +1 -1
  25. package/src/test/execute_steps.js +10 -18
  26. package/src/test/execute_test_plan.js +12 -60
  27. package/src/test/logs_file_execution.js +74 -28
  28. package/dist/js/script_type_module_supervisor.js +0 -109
  29. package/src/plugins/supervisor/client/script_type_module_supervisor.js +0 -98
@@ -1,68 +1,19 @@
1
1
  /*
2
- * Jsenv needs to wait for all js execution inside an HTML page before killing the browser.
3
- * A naive approach would consider execution done when "load" event is dispatched on window but:
4
- *
5
- * scenario | covered by window "load"
6
- * ------------------------------------------- | -------------------------
7
- * js referenced by <script src> | yes
8
- * js inlined into <script> | yes
9
- * js referenced by <script type="module" src> | partially (not for import and top level await)
10
- * js inlined into <script type="module"> | not at all
11
- *
12
2
  * This plugin provides a way for jsenv to know when js execution is done
13
- * As a side effect this plugin enables ability to hot reload js inlined into <script hot-accept>
14
- *
15
- * <script src="file.js">
16
- * becomes
17
- * <script>
18
- * window.__supervisor__.superviseScript({ src: 'file.js' })
19
- * </script>
20
- *
21
- * <script>
22
- * console.log(42)
23
- * </script>
24
- * becomes
25
- * <script>
26
- * window.__supervisor__.superviseScript({ src: 'main.html@L10-L13.js' })
27
- * </script>
28
- *
29
- * <script type="module" src="module.js"></script>
30
- * becomes
31
- * <script type="module">
32
- * import { superviseScriptTypeModule } from 'supervisor'
33
- * superviseScriptTypeModule({ src: "module.js" })
34
- * </script>
35
- *
36
- * <script type="module">
37
- * console.log(42)
38
- * </script>
39
- * becomes
40
- * <script type="module">
41
- * import { superviseScriptTypeModule } from 'supervisor'
42
- * superviseScriptTypeModule({ src: 'main.html@L10-L13.js' })
43
- * </script>
44
3
  */
45
4
 
46
5
  import { fileURLToPath } from "node:url"
47
- import {
48
- parseHtmlString,
49
- stringifyHtmlAst,
50
- visitHtmlNodes,
51
- getHtmlNodeAttribute,
52
- setHtmlNodeAttributes,
53
- analyzeScriptNode,
54
- injectScriptNodeAsEarlyAsPossible,
55
- createHtmlNode,
56
- getHtmlNodePosition,
57
- getHtmlNodeText,
58
- removeHtmlNodeText,
59
- setHtmlNodeText,
60
- } from "@jsenv/ast"
61
- import { generateInlineContentUrl, stringifyUrlSite } from "@jsenv/urls"
62
6
  import { getOriginalPosition } from "@jsenv/sourcemap"
7
+ import { stringifyUrlSite } from "@jsenv/urls"
63
8
 
9
+ import { injectSupervisorIntoHTML } from "./html_supervisor_injection.js"
64
10
  import { requireFromJsenv } from "@jsenv/core/src/require_from_jsenv.js"
65
11
 
12
+ export const supervisorFileUrl = new URL(
13
+ "./client/supervisor.js",
14
+ import.meta.url,
15
+ ).href
16
+
66
17
  export const jsenvPluginSupervisor = ({
67
18
  logs = false,
68
19
  measurePerf = false,
@@ -70,13 +21,6 @@ export const jsenvPluginSupervisor = ({
70
21
  openInEditor = true,
71
22
  errorBaseUrl,
72
23
  }) => {
73
- const supervisorFileUrl = new URL("./client/supervisor.js", import.meta.url)
74
- .href
75
- const scriptTypeModuleSupervisorFileUrl = new URL(
76
- "./client/script_type_module_supervisor.js",
77
- import.meta.url,
78
- ).href
79
-
80
24
  return {
81
25
  name: "jsenv:supervisor",
82
26
  appliesDuring: "dev",
@@ -216,184 +160,55 @@ export const jsenvPluginSupervisor = ({
216
160
  },
217
161
  transformUrlContent: {
218
162
  html: ({ url, content }, context) => {
219
- const htmlAst = parseHtmlString(content)
220
- const scriptsToSupervise = []
221
-
222
- const handleInlineScript = (node, htmlNodeText) => {
223
- const { type, extension } = analyzeScriptNode(node)
224
- const { line, column, lineEnd, columnEnd, isOriginal } =
225
- getHtmlNodePosition(node, { preferOriginal: true })
226
- let inlineScriptUrl = generateInlineContentUrl({
227
- url,
228
- extension: extension || ".js",
229
- line,
230
- column,
231
- lineEnd,
232
- columnEnd,
233
- })
234
- const [inlineScriptReference] = context.referenceUtils.foundInline({
235
- type: "script",
236
- subtype: "inline",
237
- expectedType: type,
238
- isOriginalPosition: isOriginal,
239
- specifierLine: line - 1,
240
- specifierColumn: column,
241
- specifier: inlineScriptUrl,
242
- contentType: "text/javascript",
243
- content: htmlNodeText,
244
- })
245
- removeHtmlNodeText(node)
246
- if (extension) {
247
- setHtmlNodeAttributes(node, {
248
- type: type === "js_module" ? "module" : undefined,
249
- })
250
- }
251
- scriptsToSupervise.push({
252
- node,
253
- isInline: true,
254
- type,
255
- src: inlineScriptReference.generatedSpecifier,
256
- })
257
- }
258
- const handleScriptWithSrc = (node, src) => {
259
- const { type } = analyzeScriptNode(node)
260
- const integrity = getHtmlNodeAttribute(node, "integrity")
261
- const crossorigin =
262
- getHtmlNodeAttribute(node, "crossorigin") !== undefined
263
- const defer = getHtmlNodeAttribute(node, "defer") !== undefined
264
- const async = getHtmlNodeAttribute(node, "async") !== undefined
265
- scriptsToSupervise.push({
266
- node,
267
- type,
268
- src,
269
- defer,
270
- async,
271
- integrity,
272
- crossorigin,
273
- })
274
- }
275
- visitHtmlNodes(htmlAst, {
276
- script: (node) => {
277
- const { type } = analyzeScriptNode(node)
278
- if (type !== "js_classic" && type !== "js_module") {
279
- return
280
- }
281
- if (
282
- getHtmlNodeAttribute(node, "jsenv-cooked-by") ||
283
- getHtmlNodeAttribute(node, "jsenv-inlined-by") ||
284
- getHtmlNodeAttribute(node, "jsenv-injected-by")
285
- ) {
286
- return
287
- }
288
- const noSupervisor = getHtmlNodeAttribute(node, "no-supervisor")
289
- if (noSupervisor !== undefined) {
290
- return
291
- }
292
- const htmlNodeText = getHtmlNodeText(node)
293
- if (htmlNodeText) {
294
- handleInlineScript(node, htmlNodeText)
295
- return
296
- }
297
- const src = getHtmlNodeAttribute(node, "src")
298
- if (src) {
299
- handleScriptWithSrc(node, src)
300
- return
301
- }
302
- },
303
- })
304
- const [scriptTypeModuleSupervisorFileReference] =
305
- context.referenceUtils.inject({
306
- type: "js_import",
307
- expectedType: "js_module",
308
- specifier: scriptTypeModuleSupervisorFileUrl,
309
- })
310
163
  const [supervisorFileReference] = context.referenceUtils.inject({
311
164
  type: "script",
312
165
  expectedType: "js_classic",
313
166
  specifier: supervisorFileUrl,
314
167
  })
315
- injectScriptNodeAsEarlyAsPossible(
316
- htmlAst,
317
- createHtmlNode({
318
- tagName: "script",
319
- textContent: `
320
- window.__supervisor__.setup(${JSON.stringify(
321
- {
322
- rootDirectoryUrl: context.rootDirectoryUrl,
323
- errorBaseUrl,
324
- logs,
325
- measurePerf,
326
- errorOverlay,
327
- openInEditor,
328
- },
329
- null,
330
- " ",
331
- )})
332
- `,
333
- }),
334
- "jsenv:supervisor",
335
- )
336
- injectScriptNodeAsEarlyAsPossible(
337
- htmlAst,
338
- createHtmlNode({
339
- tagName: "script",
340
- src: supervisorFileReference.generatedSpecifier,
341
- }),
342
- "jsenv:supervisor",
343
- )
344
168
 
345
- scriptsToSupervise.forEach(
346
- ({
347
- node,
348
- isInline,
349
- type,
350
- src,
351
- defer,
352
- async,
353
- integrity,
354
- crossorigin,
355
- }) => {
356
- const paramsAsJson = JSON.stringify({
357
- src,
358
- isInline,
359
- defer,
360
- async,
361
- integrity,
362
- crossorigin,
363
- })
364
- if (type === "js_module") {
365
- setHtmlNodeText(
366
- node,
367
- `
368
- import { superviseScriptTypeModule } from ${scriptTypeModuleSupervisorFileReference.generatedSpecifier}
369
- superviseScriptTypeModule(${paramsAsJson})
370
- `,
371
- )
372
- } else {
373
- setHtmlNodeText(
374
- node,
375
- `
376
- window.__supervisor__.superviseScript(${paramsAsJson})
377
- `,
378
- )
379
- }
380
- if (src) {
381
- setHtmlNodeAttributes(node, {
382
- "jsenv-inlined-by": "jsenv:supervisor",
383
- "src": undefined,
384
- "inlined-from-src": src,
385
- })
386
- } else {
387
- setHtmlNodeAttributes(node, {
388
- "jsenv-cooked-by": "jsenv:supervisor",
389
- })
390
- }
169
+ return injectSupervisorIntoHTML(
170
+ {
171
+ content,
172
+ url,
173
+ },
174
+ {
175
+ supervisorScriptSrc: supervisorFileReference.generatedSpecifier,
176
+ supervisorOptions: {
177
+ errorBaseUrl,
178
+ logs,
179
+ measurePerf,
180
+ errorOverlay,
181
+ openInEditor,
182
+ },
183
+ webServer: {
184
+ rootDirectoryUrl: context.rootDirectoryUrl,
185
+ isJsenvDevServer: true,
186
+ },
187
+ inlineAsRemote: true,
188
+ generateInlineScriptSrc: ({
189
+ type,
190
+ textContent,
191
+ inlineScriptUrl,
192
+ isOriginal,
193
+ line,
194
+ column,
195
+ }) => {
196
+ const [inlineScriptReference] =
197
+ context.referenceUtils.foundInline({
198
+ type: "script",
199
+ subtype: "inline",
200
+ expectedType: type,
201
+ isOriginalPosition: isOriginal,
202
+ specifierLine: line - 1,
203
+ specifierColumn: column,
204
+ specifier: inlineScriptUrl,
205
+ contentType: "text/javascript",
206
+ content: textContent,
207
+ })
208
+ return inlineScriptReference.generatedSpecifier
209
+ },
391
210
  },
392
211
  )
393
- const htmlModified = stringifyHtmlAst(htmlAst)
394
- return {
395
- content: htmlModified,
396
- }
397
212
  },
398
213
  },
399
214
  }
@@ -157,7 +157,7 @@ export const jsenvPluginAsJsClassicHtml = ({
157
157
  break
158
158
  }
159
159
  } catch (e) {
160
- if (context.dev) {
160
+ if (context.dev && e.code !== "PARSE_ERROR") {
161
161
  needsSystemJs = true
162
162
  // ignore cooking error, the browser will trigger it again on fetch
163
163
  // + disable cache for this html file because when browser will reload
@@ -97,6 +97,11 @@ export const jsenvPluginBabel = ({
97
97
  const { code, map } = await applyBabelPlugins({
98
98
  babelPlugins,
99
99
  urlInfo,
100
+ options: {
101
+ generatorOpts: {
102
+ retainLines: context.dev,
103
+ },
104
+ },
100
105
  })
101
106
  return {
102
107
  content: code,
@@ -74,7 +74,7 @@ const babelPluginMetadataUsesTopLevelAwait = () => {
74
74
  programPath.traverse({
75
75
  AwaitExpression: (path) => {
76
76
  const closestFunction = path.getFunctionParent()
77
- if (!closestFunction) {
77
+ if (!closestFunction || closestFunction.type === "Program") {
78
78
  usesTopLevelAwait = true
79
79
  path.stop()
80
80
  }
@@ -1,7 +1,6 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { memoryUsage } from "node:process"
3
3
  import { takeCoverage } from "node:v8"
4
- import wrapAnsi from "wrap-ansi"
5
4
  import stripAnsi from "strip-ansi"
6
5
 
7
6
  import { urlToFileSystemPath } from "@jsenv/urls"
@@ -31,8 +30,7 @@ export const executeSteps = async (
31
30
  completedExecutionLogMerging,
32
31
  completedExecutionLogAbbreviation,
33
32
  rootDirectoryUrl,
34
- devServerOrigin,
35
- sourceDirectoryUrl,
33
+ webServer,
36
34
 
37
35
  keepRunning,
38
36
  defaultMsAllocatedPerExecution,
@@ -119,8 +117,7 @@ export const executeSteps = async (
119
117
 
120
118
  let runtimeParams = {
121
119
  rootDirectoryUrl,
122
- devServerOrigin,
123
- sourceDirectoryUrl,
120
+ webServer,
124
121
 
125
122
  coverageEnabled,
126
123
  coverageConfig,
@@ -279,7 +276,7 @@ export const executeSteps = async (
279
276
  global.gc()
280
277
  }
281
278
  if (executionLogsEnabled) {
282
- let log = createExecutionLog(afterExecutionInfo, {
279
+ const log = createExecutionLog(afterExecutionInfo, {
283
280
  completedExecutionLogAbbreviation,
284
281
  counters,
285
282
  logRuntime,
@@ -293,16 +290,6 @@ export const executeSteps = async (
293
290
  ? { memoryHeap: memoryUsage().heapUsed }
294
291
  : {}),
295
292
  })
296
- log = `${log}
297
-
298
- `
299
- const { columns = 80 } = process.stdout
300
- log = wrapAnsi(log, columns, {
301
- trim: false,
302
- hard: true,
303
- wordWrap: false,
304
- })
305
-
306
293
  // replace spinner with this execution result
307
294
  if (spinner) spinner.stop()
308
295
  executionLog.write(log)
@@ -319,11 +306,16 @@ export const executeSteps = async (
319
306
  executionLog = createLog({ newLine: "" })
320
307
  }
321
308
  }
322
- if (
309
+ const isLastExecutionLog = executionIndex === executionSteps.length - 1
310
+ const cancelRemaining =
323
311
  failFast &&
324
312
  executionResult.status !== "completed" &&
325
313
  counters.done < counters.total
326
- ) {
314
+ if (isLastExecutionLog) {
315
+ executionLog.write("\n")
316
+ }
317
+
318
+ if (cancelRemaining) {
327
319
  logger.info(`"failFast" enabled -> cancel remaining executions`)
328
320
  failFastAbortController.abort()
329
321
  }
@@ -8,8 +8,7 @@ import {
8
8
  } from "@jsenv/filesystem"
9
9
  import { createLogger, createDetailedMessage } from "@jsenv/log"
10
10
 
11
- import { pingServer } from "../ping_server.js"
12
- import { basicFetch } from "../basic_fetch.js"
11
+ import { assertAndNormalizeWebServer } from "../execute/web_server_param.js"
13
12
  import { generateCoverageJsonFile } from "./coverage/coverage_reporter_json_file.js"
14
13
  import { generateCoverageHtmlDirectory } from "./coverage/coverage_reporter_html_directory.js"
15
14
  import { generateCoverageTextLog } from "./coverage/coverage_reporter_text_log.js"
@@ -20,7 +19,7 @@ import { executeSteps } from "./execute_steps.js"
20
19
  * Execute a list of files and log how it goes.
21
20
  * @param {Object} testPlanParameters
22
21
  * @param {string|url} testPlanParameters.rootDirectoryUrl Directory containing test files;
23
- * @param {string|url} [testPlanParameters.devServerOrigin=undefined] Jsenv dev server origin; required when executing test on browsers
22
+ * @param {Object} [testPlanParameters.webServer] Web server info; required when executing test on browsers
24
23
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
25
24
  * @param {boolean} [testPlanParameters.completedExecutionLogAbbreviation=false] Abbreviate completed execution information to shorten terminal output
26
25
  * @param {boolean} [testPlanParameters.completedExecutionLogMerging=false] Merge completed execution logs to shorten terminal output
@@ -46,10 +45,9 @@ export const executeTestPlan = async ({
46
45
  logFileRelativeUrl = ".jsenv/test_plan_debug.txt",
47
46
  completedExecutionLogAbbreviation = false,
48
47
  completedExecutionLogMerging = false,
49
- rootDirectoryUrl,
50
- devServerModuleUrl,
51
- devServerOrigin,
52
48
 
49
+ rootDirectoryUrl,
50
+ webServer,
53
51
  testPlan,
54
52
  updateProcessExitCode = true,
55
53
  maxExecutionsInParallel = 1,
@@ -96,8 +94,6 @@ export const executeTestPlan = async ({
96
94
  }) => {
97
95
  let someNeedsServer = false
98
96
  let someNodeRuntime = false
99
- let stopDevServerNeeded = false
100
- let sourceDirectoryUrl
101
97
  const runtimes = {}
102
98
  // param validation
103
99
  {
@@ -137,44 +133,7 @@ export const executeTestPlan = async ({
137
133
  })
138
134
 
139
135
  if (someNeedsServer) {
140
- if (!devServerOrigin) {
141
- throw new TypeError(
142
- `devServerOrigin is required when running tests on browser(s)`,
143
- )
144
- }
145
- let devServerStarted = await pingServer(devServerOrigin)
146
- if (!devServerStarted) {
147
- if (!devServerModuleUrl) {
148
- throw new TypeError(
149
- `devServerModuleUrl is required when dev server is not started in order to run tests on browser(s)`,
150
- )
151
- }
152
- try {
153
- process.env.IMPORTED_BY_TEST_PLAN = "1"
154
- await import(devServerModuleUrl)
155
- delete process.env.IMPORTED_BY_TEST_PLAN
156
- } catch (e) {
157
- if (e.code === "ERR_MODULE_NOT_FOUND") {
158
- throw new Error(
159
- `Cannot find file responsible to start dev server at "${devServerModuleUrl}"`,
160
- )
161
- }
162
- throw e
163
- }
164
- devServerStarted = await pingServer(devServerOrigin)
165
- if (!devServerStarted) {
166
- throw new Error(
167
- `dev server not started after importing "${devServerModuleUrl}", ensure this module file is starting a server at "${devServerOrigin}"`,
168
- )
169
- }
170
- stopDevServerNeeded = true
171
- }
172
-
173
- const devServerParams = await basicFetch(
174
- `${devServerOrigin}/__server_params__.json`,
175
- { rejectUnauthorized: false },
176
- )
177
- sourceDirectoryUrl = devServerParams.sourceDirectoryUrl
136
+ await assertAndNormalizeWebServer(webServer)
178
137
  }
179
138
 
180
139
  if (coverageEnabled) {
@@ -290,7 +249,12 @@ export const executeTestPlan = async ({
290
249
  }
291
250
  }
292
251
 
293
- testPlan = { ...testPlan, "**/.jsenv/": null }
252
+ testPlan = {
253
+ "file:///**/node_modules/": null,
254
+ "**/*./": null,
255
+ ...testPlan,
256
+ "**/.jsenv/": null,
257
+ }
294
258
  logger.debug(`Generate executions`)
295
259
  const executionSteps = await executionStepsFromTestPlan({
296
260
  signal,
@@ -313,8 +277,7 @@ export const executeTestPlan = async ({
313
277
  completedExecutionLogMerging,
314
278
  completedExecutionLogAbbreviation,
315
279
  rootDirectoryUrl,
316
- devServerOrigin,
317
- sourceDirectoryUrl,
280
+ webServer,
318
281
 
319
282
  maxExecutionsInParallel,
320
283
  defaultMsAllocatedPerExecution,
@@ -331,17 +294,6 @@ export const executeTestPlan = async ({
331
294
  coverageV8ConflictWarning,
332
295
  coverageTempDirectoryUrl,
333
296
  })
334
- if (stopDevServerNeeded) {
335
- // we are expecting ECONNRESET because server will be stopped by the request
336
- basicFetch(`${devServerOrigin}/__stop__`, {
337
- rejectUnauthorized: false,
338
- }).catch((e) => {
339
- if (e.code === "ECONNRESET") {
340
- return
341
- }
342
- throw e
343
- })
344
- }
345
297
  if (
346
298
  updateProcessExitCode &&
347
299
  result.planSummary.counters.total !== result.planSummary.counters.completed