@jsenv/core 33.0.2 → 34.0.1

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 (32) hide show
  1. package/dist/js/autoreload.js +1 -4
  2. package/dist/js/supervisor.js +498 -290
  3. package/dist/jsenv.js +938 -370
  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 +283 -0
  21. package/src/plugins/supervisor/jsenv_plugin_supervisor.js +48 -233
  22. package/src/plugins/transpilation/as_js_classic/convert_js_module_to_js_classic.js +67 -30
  23. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +1 -1
  24. package/src/plugins/transpilation/babel/jsenv_plugin_babel.js +5 -0
  25. package/src/plugins/transpilation/jsenv_plugin_top_level_await.js +1 -1
  26. package/src/test/execute_steps.js +10 -18
  27. package/src/test/execute_test_plan.js +12 -60
  28. package/src/test/logs_file_execution.js +74 -28
  29. package/dist/js/babel_plugin_transform_modules_systemjs.cjs +0 -392
  30. package/dist/js/script_type_module_supervisor.js +0 -109
  31. package/src/plugins/supervisor/client/script_type_module_supervisor.js +0 -98
  32. package/src/plugins/transpilation/as_js_classic/babel_plugin_transform_modules_systemjs.cjs +0 -608
@@ -0,0 +1,281 @@
1
+ /*
2
+ * Jsenv needs to track js execution in order to:
3
+ * 1. report errors
4
+ * 2. wait for all js execution inside an HTML page before killing the browser
5
+ *
6
+ * A naive approach would rely on "load" events on window but:
7
+ * scenario | covered by window "load"
8
+ * ------------------------------------------- | -------------------------
9
+ * js referenced by <script src> | yes
10
+ * js inlined into <script> | yes
11
+ * js referenced by <script type="module" src> | partially (not for import and top level await)
12
+ * js inlined into <script type="module"> | not at all
13
+ * Same for "error" event on window who is not enough
14
+ *
15
+ * <script src="file.js">
16
+ * becomes
17
+ * <script>
18
+ * window.__supervisor__.superviseScript('file.js')
19
+ * </script>
20
+ *
21
+ * <script>
22
+ * console.log(42)
23
+ * </script>
24
+ * becomes
25
+ * <script inlined-from-src="main.html@L10-C5.js">
26
+ * window.__supervisor.__superviseScript("main.html@L10-C5.js")
27
+ * </script>
28
+ *
29
+ * <script type="module" src="module.js"></script>
30
+ * becomes
31
+ * <script type="module">
32
+ * window.__supervisor__.superviseScriptTypeModule('module.js')
33
+ * </script>
34
+ *
35
+ * <script type="module">
36
+ * console.log(42)
37
+ * </script>
38
+ * becomes
39
+ * <script type="module" inlined-from-src="main.html@L10-C5.js">
40
+ * window.__supervisor__.superviseScriptTypeModule('main.html@L10-C5.js')
41
+ * </script>
42
+ *
43
+ * Why Inline scripts are converted to files dynamically?
44
+ * -> No changes required on js source code, it's only the HTML that is modified
45
+ * - Also allow to catch syntax errors and export missing
46
+ */
47
+
48
+ import {
49
+ parseHtmlString,
50
+ stringifyHtmlAst,
51
+ visitHtmlNodes,
52
+ getHtmlNodeAttribute,
53
+ setHtmlNodeAttributes,
54
+ analyzeScriptNode,
55
+ injectScriptNodeAsEarlyAsPossible,
56
+ createHtmlNode,
57
+ getHtmlNodePosition,
58
+ getHtmlNodeText,
59
+ setHtmlNodeText,
60
+ } from "@jsenv/ast"
61
+ import { generateInlineContentUrl, urlToRelativeUrl } from "@jsenv/urls"
62
+
63
+ import { injectSupervisorIntoJs } from "./js_supervisor_injection.js"
64
+
65
+ export const supervisorFileUrl = new URL(
66
+ "./client/supervisor.js",
67
+ import.meta.url,
68
+ ).href
69
+
70
+ export const injectSupervisorIntoHTML = async (
71
+ { content, url },
72
+ {
73
+ supervisorScriptSrc = supervisorFileUrl,
74
+ supervisorOptions,
75
+ webServer,
76
+ onInlineScript = () => {},
77
+ generateInlineScriptSrc = ({ inlineScriptUrl }) =>
78
+ urlToRelativeUrl(inlineScriptUrl, webServer.rootDirectoryUrl),
79
+ inlineAsRemote,
80
+ },
81
+ ) => {
82
+ const htmlAst = parseHtmlString(content)
83
+ const mutations = []
84
+ const actions = []
85
+
86
+ const scriptInfos = []
87
+ // 1. Find inline and remote scripts
88
+ {
89
+ const handleInlineScript = (
90
+ scriptNode,
91
+ { type, extension, textContent },
92
+ ) => {
93
+ const { line, column, lineEnd, columnEnd, isOriginal } =
94
+ getHtmlNodePosition(scriptNode, { preferOriginal: true })
95
+ const inlineScriptUrl = generateInlineContentUrl({
96
+ url,
97
+ extension: extension || ".js",
98
+ line,
99
+ column,
100
+ lineEnd,
101
+ columnEnd,
102
+ })
103
+ const inlineScriptSrc = generateInlineScriptSrc({
104
+ type,
105
+ textContent,
106
+ inlineScriptUrl,
107
+ isOriginal,
108
+ line,
109
+ column,
110
+ })
111
+ onInlineScript({
112
+ type,
113
+ textContent,
114
+ url: inlineScriptUrl,
115
+ isOriginal,
116
+ line,
117
+ column,
118
+ src: inlineScriptSrc,
119
+ })
120
+ if (inlineAsRemote) {
121
+ // prefere la version src
122
+ scriptInfos.push({ type, src: inlineScriptSrc })
123
+ const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
124
+ type,
125
+ src: inlineScriptSrc,
126
+ })
127
+ mutations.push(() => {
128
+ setHtmlNodeText(scriptNode, remoteJsSupervised)
129
+ setHtmlNodeAttributes(scriptNode, {
130
+ "jsenv-cooked-by": "jsenv:supervisor",
131
+ "src": undefined,
132
+ "inlined-from-src": inlineScriptSrc,
133
+ })
134
+ })
135
+ } else {
136
+ scriptInfos.push({ type, src: inlineScriptSrc, isInline: true })
137
+ actions.push(async () => {
138
+ try {
139
+ const inlineJsSupervised = await injectSupervisorIntoJs({
140
+ webServer,
141
+ content: textContent,
142
+ url: inlineScriptUrl,
143
+ type,
144
+ inlineSrc: inlineScriptSrc,
145
+ })
146
+ mutations.push(() => {
147
+ setHtmlNodeText(scriptNode, inlineJsSupervised)
148
+ setHtmlNodeAttributes(scriptNode, {
149
+ "jsenv-cooked-by": "jsenv:supervisor",
150
+ })
151
+ })
152
+ } catch (e) {
153
+ if (e.code === "PARSE_ERROR") {
154
+ // mutations.push(() => {
155
+ // setHtmlNodeAttributes(scriptNode, {
156
+ // "jsenv-cooked-by": "jsenv:supervisor",
157
+ // })
158
+ // })
159
+ // on touche a rien
160
+ return
161
+ }
162
+ throw e
163
+ }
164
+ })
165
+ }
166
+ }
167
+ const handleScriptWithSrc = (scriptNode, { type, src }) => {
168
+ scriptInfos.push({ type, src })
169
+ const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
170
+ type,
171
+ src,
172
+ })
173
+ mutations.push(() => {
174
+ setHtmlNodeText(scriptNode, remoteJsSupervised)
175
+ setHtmlNodeAttributes(scriptNode, {
176
+ "jsenv-cooked-by": "jsenv:supervisor",
177
+ "src": undefined,
178
+ "inlined-from-src": src,
179
+ })
180
+ })
181
+ }
182
+ visitHtmlNodes(htmlAst, {
183
+ script: (scriptNode) => {
184
+ const { type, extension } = analyzeScriptNode(scriptNode)
185
+ if (type !== "js_classic" && type !== "js_module") {
186
+ return
187
+ }
188
+ if (getHtmlNodeAttribute(scriptNode, "jsenv-injected-by")) {
189
+ return
190
+ }
191
+ const noSupervisor = getHtmlNodeAttribute(scriptNode, "no-supervisor")
192
+ if (noSupervisor !== undefined) {
193
+ return
194
+ }
195
+
196
+ const scriptNodeText = getHtmlNodeText(scriptNode)
197
+ if (scriptNodeText) {
198
+ handleInlineScript(scriptNode, {
199
+ type,
200
+ extension,
201
+ textContent: scriptNodeText,
202
+ })
203
+ return
204
+ }
205
+ const src = getHtmlNodeAttribute(scriptNode, "src")
206
+ if (src) {
207
+ handleScriptWithSrc(scriptNode, { type, src })
208
+ return
209
+ }
210
+ },
211
+ })
212
+ }
213
+ // 2. Inject supervisor js file + setup call
214
+ {
215
+ const setupParamsSource = stringifyParams(
216
+ {
217
+ ...supervisorOptions,
218
+ serverIsJsenvDevServer: webServer.isJsenvDevServer,
219
+ rootDirectoryUrl: webServer.rootDirectoryUrl,
220
+ scriptInfos,
221
+ },
222
+ " ",
223
+ )
224
+ injectScriptNodeAsEarlyAsPossible(
225
+ htmlAst,
226
+ createHtmlNode({
227
+ tagName: "script",
228
+ textContent: `
229
+ window.__supervisor__.setup({
230
+ ${setupParamsSource}
231
+ })
232
+ `,
233
+ }),
234
+ "jsenv:supervisor",
235
+ )
236
+ const supervisorScript = createHtmlNode({
237
+ tagName: "script",
238
+ src: supervisorScriptSrc,
239
+ })
240
+ injectScriptNodeAsEarlyAsPossible(
241
+ htmlAst,
242
+ supervisorScript,
243
+ "jsenv:supervisor",
244
+ )
245
+ }
246
+ // 3. Perform actions (transforming inline script content) and html mutations
247
+ if (actions.length > 0) {
248
+ await Promise.all(actions.map((action) => action()))
249
+ }
250
+ mutations.forEach((mutation) => mutation())
251
+ const htmlModified = stringifyHtmlAst(htmlAst)
252
+ return {
253
+ content: htmlModified,
254
+ }
255
+ }
256
+
257
+ const stringifyParams = (params, prefix = "") => {
258
+ const source = JSON.stringify(params, null, prefix)
259
+ if (prefix.length) {
260
+ // remove leading "{\n"
261
+ // remove leading prefix
262
+ // remove trailing "\n}"
263
+ return source.slice(2 + prefix.length, -2)
264
+ }
265
+ // remove leading "{"
266
+ // remove trailing "}"
267
+ return source.slice(1, -1)
268
+ }
269
+
270
+ const generateCodeToSuperviseScriptWithSrc = ({ type, src }) => {
271
+ if (type === "js_module") {
272
+ return `
273
+ window.__supervisor__.superviseScriptTypeModule(${JSON.stringify(
274
+ src,
275
+ )}, (url) => import(url));
276
+ `
277
+ }
278
+ return `
279
+ window.__supervisor__.superviseScript(${JSON.stringify(src)});
280
+ `
281
+ }
@@ -0,0 +1,283 @@
1
+ /*
2
+ * ```js
3
+ * console.log(42)
4
+ * ```
5
+ * becomes
6
+ * ```js
7
+ * window.__supervisor__.jsClassicStart('main.html@L10-L13.js')
8
+ * try {
9
+ * console.log(42)
10
+ * window.__supervisor__.jsClassicEnd('main.html@L10-L13.js')
11
+ * } catch(e) {
12
+ * window.__supervisor__.jsClassicError('main.html@L10-L13.js', e)
13
+ * }
14
+ * ```
15
+ *
16
+ * ```js
17
+ * import value from "./file.js"
18
+ * console.log(value)
19
+ * ```
20
+ * becomes
21
+ * ```js
22
+ * window.__supervisor__.jsModuleStart('main.html@L10-L13.js')
23
+ * try {
24
+ * const value = await import("./file.js")
25
+ * console.log(value)
26
+ * window.__supervisor__.jsModuleEnd('main.html@L10-L13.js')
27
+ * } catch(e) {
28
+ * window.__supervisor__.jsModuleError('main.html@L10-L13.js', e)
29
+ * }
30
+ * ```
31
+ *
32
+ * -> TO KEEP IN MIND:
33
+ * Static import can throw errors like
34
+ * The requested module '/js_module_export_not_found/foo.js' does not provide an export named 'answerr'
35
+ * While dynamic import will work just fine
36
+ * and create a variable named "undefined"
37
+ */
38
+
39
+ import { urlToRelativeUrl } from "@jsenv/urls"
40
+ import { applyBabelPlugins } from "@jsenv/ast"
41
+ import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap"
42
+
43
+ export const injectSupervisorIntoJs = async ({
44
+ webServer,
45
+ content,
46
+ url,
47
+ type,
48
+ inlineSrc,
49
+ }) => {
50
+ const babelPluginJsSupervisor =
51
+ type === "js_module"
52
+ ? babelPluginJsModuleSupervisor
53
+ : babelPluginJsClassicSupervisor
54
+ const result = await applyBabelPlugins({
55
+ urlInfo: {
56
+ content,
57
+ originalUrl: url,
58
+ type,
59
+ },
60
+ babelPlugins: [[babelPluginJsSupervisor, { inlineSrc }]],
61
+ })
62
+ let code = result.code
63
+ let map = result.map
64
+ const sourcemapDataUrl = generateSourcemapDataUrl(map)
65
+ code = SOURCEMAP.writeComment({
66
+ contentType: "text/javascript",
67
+ content: code,
68
+ specifier: sourcemapDataUrl,
69
+ })
70
+ code = `${code}
71
+ //# sourceURL=${urlToRelativeUrl(url, webServer.rootDirectoryUrl)}`
72
+ return code
73
+ }
74
+
75
+ const babelPluginJsModuleSupervisor = (babel) => {
76
+ const t = babel.types
77
+
78
+ return {
79
+ name: "js-module-supervisor",
80
+ visitor: {
81
+ Program: (programPath, state) => {
82
+ const { inlineSrc } = state.opts
83
+ if (state.file.metadata.jsExecutionInstrumented) return
84
+ state.file.metadata.jsExecutionInstrumented = true
85
+
86
+ const urlNode = t.stringLiteral(inlineSrc)
87
+ const startCallNode = createSupervisionCall({
88
+ t,
89
+ urlNode,
90
+ methodName: "jsModuleStart",
91
+ })
92
+ const endCallNode = createSupervisionCall({
93
+ t,
94
+ urlNode,
95
+ methodName: "jsModuleEnd",
96
+ })
97
+ const errorCallNode = createSupervisionCall({
98
+ t,
99
+ urlNode,
100
+ methodName: "jsModuleError",
101
+ args: [t.identifier("e")],
102
+ })
103
+
104
+ const bodyPath = programPath.get("body")
105
+ const importNodes = []
106
+ const topLevelNodes = []
107
+ for (const topLevelNodePath of bodyPath) {
108
+ const topLevelNode = topLevelNodePath.node
109
+ if (t.isImportDeclaration(topLevelNode)) {
110
+ importNodes.push(topLevelNode)
111
+ } else {
112
+ topLevelNodes.push(topLevelNode)
113
+ }
114
+ }
115
+
116
+ // replace all import nodes with dynamic imports
117
+ const dynamicImports = []
118
+ importNodes.forEach((importNode) => {
119
+ const dynamicImportConversion = convertStaticImportIntoDynamicImport(
120
+ importNode,
121
+ t,
122
+ )
123
+ if (Array.isArray(dynamicImportConversion)) {
124
+ dynamicImports.push(...dynamicImportConversion)
125
+ } else {
126
+ dynamicImports.push(dynamicImportConversion)
127
+ }
128
+ })
129
+
130
+ const tryCatchNode = t.tryStatement(
131
+ t.blockStatement([...dynamicImports, ...topLevelNodes, endCallNode]),
132
+ t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])),
133
+ )
134
+ programPath.replaceWith(t.program([startCallNode, tryCatchNode]))
135
+ },
136
+ },
137
+ }
138
+ }
139
+
140
+ const convertStaticImportIntoDynamicImport = (staticImportNode, t) => {
141
+ const awaitExpression = t.awaitExpression(
142
+ t.callExpression(t.import(), [
143
+ t.stringLiteral(staticImportNode.source.value),
144
+ ]),
145
+ )
146
+
147
+ // import "./file.js" -> await import("./file.js")
148
+ if (staticImportNode.specifiers.length === 0) {
149
+ return t.expressionStatement(awaitExpression)
150
+ }
151
+ if (staticImportNode.specifiers.length === 1) {
152
+ const [firstSpecifier] = staticImportNode.specifiers
153
+ if (firstSpecifier.type === "ImportNamespaceSpecifier") {
154
+ return t.variableDeclaration("const", [
155
+ t.variableDeclarator(
156
+ t.identifier(firstSpecifier.local.name),
157
+ awaitExpression,
158
+ ),
159
+ ])
160
+ }
161
+ }
162
+ if (staticImportNode.specifiers.length === 2) {
163
+ const [first, second] = staticImportNode.specifiers
164
+ if (
165
+ first.type === "ImportDefaultSpecifier" &&
166
+ second.type === "ImportNamespaceSpecifier"
167
+ ) {
168
+ const namespaceDeclaration = t.variableDeclaration("const", [
169
+ t.variableDeclarator(t.identifier(second.local.name), awaitExpression),
170
+ ])
171
+ const defaultDeclaration = t.variableDeclaration("const", [
172
+ t.variableDeclarator(
173
+ t.identifier(first.local.name),
174
+ t.memberExpression(
175
+ t.identifier(second.local.name),
176
+ t.identifier("default"),
177
+ ),
178
+ ),
179
+ ])
180
+ return [namespaceDeclaration, defaultDeclaration]
181
+ }
182
+ }
183
+
184
+ // import { name } from "./file.js" -> const { name } = await import("./file.js")
185
+ // import toto, { name } from "./file.js" -> const { name, default as toto } = await import("./file.js")
186
+ const objectPattern = t.objectPattern(
187
+ staticImportNode.specifiers.map((specifier) => {
188
+ if (specifier.type === "ImportDefaultSpecifier") {
189
+ return t.objectProperty(
190
+ t.identifier("default"),
191
+ t.identifier(specifier.local.name),
192
+ false, // computed
193
+ false, // shorthand
194
+ )
195
+ }
196
+ // if (specifier.type === "ImportNamespaceSpecifier") {
197
+ // return t.restElement(t.identifier(specifier.local.name))
198
+ // }
199
+ const isRenamed = specifier.imported.name !== specifier.local.name
200
+ if (isRenamed) {
201
+ return t.objectProperty(
202
+ t.identifier(specifier.imported.name),
203
+ t.identifier(specifier.local.name),
204
+ false, // computed
205
+ false, // shorthand
206
+ )
207
+ }
208
+ // shorthand must be true
209
+ return t.objectProperty(
210
+ t.identifier(specifier.local.name),
211
+ t.identifier(specifier.local.name),
212
+ false, // computed
213
+ true, // shorthand
214
+ )
215
+ }),
216
+ )
217
+ const variableDeclarator = t.variableDeclarator(
218
+ objectPattern,
219
+ awaitExpression,
220
+ )
221
+ const variableDeclaration = t.variableDeclaration("const", [
222
+ variableDeclarator,
223
+ ])
224
+ return variableDeclaration
225
+ }
226
+
227
+ const babelPluginJsClassicSupervisor = (babel) => {
228
+ const t = babel.types
229
+
230
+ return {
231
+ name: "js-classic-supervisor",
232
+ visitor: {
233
+ Program: (programPath, state) => {
234
+ const { inlineSrc } = state.opts
235
+ if (state.file.metadata.jsExecutionInstrumented) return
236
+ state.file.metadata.jsExecutionInstrumented = true
237
+
238
+ const urlNode = t.stringLiteral(inlineSrc)
239
+ const startCallNode = createSupervisionCall({
240
+ t,
241
+ urlNode,
242
+ methodName: "jsClassicStart",
243
+ })
244
+ const endCallNode = createSupervisionCall({
245
+ t,
246
+ urlNode,
247
+ methodName: "jsClassicEnd",
248
+ })
249
+ const errorCallNode = createSupervisionCall({
250
+ t,
251
+ urlNode,
252
+ methodName: "jsClassicError",
253
+ args: [t.identifier("e")],
254
+ })
255
+
256
+ const topLevelNodes = programPath.node.body
257
+ const tryCatchNode = t.tryStatement(
258
+ t.blockStatement([...topLevelNodes, endCallNode]),
259
+ t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])),
260
+ )
261
+
262
+ programPath.replaceWith(t.program([startCallNode, tryCatchNode]))
263
+ },
264
+ },
265
+ }
266
+ }
267
+
268
+ const createSupervisionCall = ({ t, methodName, urlNode, args = [] }) => {
269
+ return t.expressionStatement(
270
+ t.callExpression(
271
+ t.memberExpression(
272
+ t.memberExpression(
273
+ t.identifier("window"),
274
+ t.identifier("__supervisor__"),
275
+ ),
276
+ t.identifier(methodName),
277
+ ),
278
+ [urlNode, ...args],
279
+ ),
280
+ [],
281
+ null,
282
+ )
283
+ }