@jsenv/core 33.0.1 → 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 +874 -347
  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 +16 -61
  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,
@@ -66,7 +64,10 @@ export const executeTestPlan = async ({
66
64
 
67
65
  coverageEnabled = process.argv.includes("--coverage"),
68
66
  coverageConfig = {
69
- "./**/src/**": true,
67
+ "file:///**/.*": false,
68
+ "file:///**/.*/": false,
69
+ "file:///**/node_modules/": false,
70
+ "./**/src/": true,
70
71
  "./**/tests/": false,
71
72
  "./**/*.test.html": false,
72
73
  "./**/*.test.js": false,
@@ -93,8 +94,6 @@ export const executeTestPlan = async ({
93
94
  }) => {
94
95
  let someNeedsServer = false
95
96
  let someNodeRuntime = false
96
- let stopDevServerNeeded = false
97
- let sourceDirectoryUrl
98
97
  const runtimes = {}
99
98
  // param validation
100
99
  {
@@ -134,44 +133,7 @@ export const executeTestPlan = async ({
134
133
  })
135
134
 
136
135
  if (someNeedsServer) {
137
- if (!devServerOrigin) {
138
- throw new TypeError(
139
- `devServerOrigin is required when running tests on browser(s)`,
140
- )
141
- }
142
- let devServerStarted = await pingServer(devServerOrigin)
143
- if (!devServerStarted) {
144
- if (!devServerModuleUrl) {
145
- throw new TypeError(
146
- `devServerModuleUrl is required when dev server is not started in order to run tests on browser(s)`,
147
- )
148
- }
149
- try {
150
- process.env.IMPORTED_BY_TEST_PLAN = "1"
151
- await import(devServerModuleUrl)
152
- delete process.env.IMPORTED_BY_TEST_PLAN
153
- } catch (e) {
154
- if (e.code === "ERR_MODULE_NOT_FOUND") {
155
- throw new Error(
156
- `Cannot find file responsible to start dev server at "${devServerModuleUrl}"`,
157
- )
158
- }
159
- throw e
160
- }
161
- devServerStarted = await pingServer(devServerOrigin)
162
- if (!devServerStarted) {
163
- throw new Error(
164
- `dev server not started after importing "${devServerModuleUrl}", ensure this module file is starting a server at "${devServerOrigin}"`,
165
- )
166
- }
167
- stopDevServerNeeded = true
168
- }
169
-
170
- const devServerParams = await basicFetch(
171
- `${devServerOrigin}/__server_params__.json`,
172
- { rejectUnauthorized: false },
173
- )
174
- sourceDirectoryUrl = devServerParams.sourceDirectoryUrl
136
+ await assertAndNormalizeWebServer(webServer)
175
137
  }
176
138
 
177
139
  if (coverageEnabled) {
@@ -287,7 +249,12 @@ export const executeTestPlan = async ({
287
249
  }
288
250
  }
289
251
 
290
- testPlan = { ...testPlan, "**/.jsenv/": null }
252
+ testPlan = {
253
+ "file:///**/node_modules/": null,
254
+ "**/*./": null,
255
+ ...testPlan,
256
+ "**/.jsenv/": null,
257
+ }
291
258
  logger.debug(`Generate executions`)
292
259
  const executionSteps = await executionStepsFromTestPlan({
293
260
  signal,
@@ -310,8 +277,7 @@ export const executeTestPlan = async ({
310
277
  completedExecutionLogMerging,
311
278
  completedExecutionLogAbbreviation,
312
279
  rootDirectoryUrl,
313
- devServerOrigin,
314
- sourceDirectoryUrl,
280
+ webServer,
315
281
 
316
282
  maxExecutionsInParallel,
317
283
  defaultMsAllocatedPerExecution,
@@ -328,17 +294,6 @@ export const executeTestPlan = async ({
328
294
  coverageV8ConflictWarning,
329
295
  coverageTempDirectoryUrl,
330
296
  })
331
- if (stopDevServerNeeded) {
332
- // we are expecting ECONNRESET because server will be stopped by the request
333
- basicFetch(`${devServerOrigin}/__stop__`, {
334
- rejectUnauthorized: false,
335
- }).catch((e) => {
336
- if (e.code === "ECONNRESET") {
337
- return
338
- }
339
- throw e
340
- })
341
- }
342
297
  if (
343
298
  updateProcessExitCode &&
344
299
  result.planSummary.counters.total !== result.planSummary.counters.completed