@jsenv/core 27.0.0-alpha.36 → 27.0.0-alpha.39

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 (28) hide show
  1. package/main.js +4 -1
  2. package/package.json +10 -12
  3. package/src/build/build.js +7 -0
  4. package/src/build/build_urls_generator.js +15 -2
  5. package/src/build/start_build_server.js +4 -0
  6. package/src/omega/kitchen.js +4 -2
  7. package/src/omega/server/file_service.js +13 -12
  8. package/src/omega/url_graph/url_info_transformations.js +6 -1
  9. package/src/omega/url_graph.js +1 -0
  10. package/src/plugins/autoreload/dev_sse/jsenv_plugin_dev_sse_server.js +2 -3
  11. package/src/plugins/commonjs_globals/jsenv_plugin_commonjs_globals.js +7 -1
  12. package/src/plugins/filesystem_magic/jsenv_plugin_filesystem_magic.js +1 -1
  13. package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +3 -0
  14. package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +7 -0
  15. package/src/plugins/inject_globals/jsenv_plugin_inject_globals.js +48 -6
  16. package/src/plugins/inline/jsenv_plugin_js_inline_content.js +7 -0
  17. package/src/plugins/node_esm_resolution/jsenv_plugin_node_esm_resolution.js +79 -10
  18. package/src/plugins/plugin_controller.js +21 -1
  19. package/src/plugins/plugins.js +10 -1
  20. package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +4 -0
  21. package/src/plugins/transpilation/jsenv_plugin_top_level_await.js +35 -2
  22. package/src/plugins/url_references/js/js_urls.js +47 -62
  23. package/src/plugins/url_references/jsenv_plugin_imports_analysis.js +46 -0
  24. package/src/plugins/url_references/jsenv_plugin_url_analysis.js +18 -0
  25. package/src/plugins/url_references/jsenv_plugin_url_references.js +3 -15
  26. package/src/plugins/url_version/jsenv_plugin_url_version.js +5 -8
  27. package/src/test/execute_plan.js +3 -0
  28. package/src/test/execute_test_plan.js +9 -7
package/main.js CHANGED
@@ -1,7 +1,10 @@
1
1
  // dev
2
2
  export { startDevServer } from "./src/dev/start_dev_server.js"
3
3
  // test
4
- export { executeTestPlan } from "./src/test/execute_test_plan.js"
4
+ export {
5
+ executeTestPlan,
6
+ defaultCoverageConfig,
7
+ } from "./src/test/execute_test_plan.js"
5
8
  export {
6
9
  chromium,
7
10
  chromiumIsolatedTab,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "27.0.0-alpha.36",
3
+ "version": "27.0.0-alpha.39",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,19 +31,20 @@
31
31
  "./packages/*"
32
32
  ],
33
33
  "scripts": {
34
- "eslint": "npx eslint ./test/dev_server/manual/main/importmap_inline/ --ext=.js,.mjs,.cjs,.html",
34
+ "eslint": "npx eslint . --ext=.js,.mjs,.cjs,.html",
35
+ "dev": "node ./script/dev/dev.mjs",
35
36
  "build": "node ./script/build/build.mjs",
36
37
  "test": "node ./script/test/test.mjs",
37
- "test-with-coverage": "npm run test -- --coverage",
38
- "dev": "node ./script/dev/dev.mjs",
38
+ "test-packages": "npm run test --workspaces --if-present",
39
39
  "start_file_server": "node ./script/dev/start_file_server.mjs",
40
+ "workspace-versions": "node ./script/publish/workspace_versions.mjs",
41
+ "workspace-publish": "node ./script/publish/workspace_publish.mjs",
40
42
  "performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log --once",
41
43
  "file-size": "node ./script/file_size/file_size.mjs --log",
42
44
  "prettier": "prettier --write .",
43
45
  "playwright-install": "npx playwright install-deps && npx playwright install",
44
46
  "certificate-install": "node ./script/dev/install_certificate_authority.mjs",
45
- "workspace-versions": "node ./script/publish/workspace_versions.mjs",
46
- "workspace-publish": "node ./script/publish/workspace_publish.mjs",
47
+ "test-with-coverage": "npm run test -- --coverage",
47
48
  "prepublishOnly": "npm run build"
48
49
  },
49
50
  "optionalDependencies": {
@@ -63,10 +64,10 @@
63
64
  "@jsenv/integrity": "0.0.1",
64
65
  "@jsenv/log": "1.5.2",
65
66
  "@jsenv/logger": "4.0.1",
66
- "@jsenv/node-esm-resolution": "0.0.4",
67
+ "@jsenv/node-esm-resolution": "0.0.6",
67
68
  "@jsenv/server": "12.6.1",
68
69
  "@jsenv/uneval": "1.6.0",
69
- "@jsenv/utils": "1.4.4",
70
+ "@jsenv/utils": "1.5.0",
70
71
  "construct-style-sheets-polyfill": "3.1.0",
71
72
  "cssnano": "5.1.7",
72
73
  "cssnano-preset-default": "5.2.7",
@@ -93,7 +94,7 @@
93
94
  "@jsenv/file-size-impact": "12.1.11",
94
95
  "@jsenv/https-local": "1.0.8",
95
96
  "@jsenv/importmap-node-module": "5.1.3",
96
- "@jsenv/package-workspace": "0.0.5",
97
+ "@jsenv/package-workspace": "0.1.1",
97
98
  "@jsenv/performance-impact": "2.2.10",
98
99
  "@jsenv/pwa": "5.0.0",
99
100
  "eslint": "8.13.0",
@@ -102,10 +103,7 @@
102
103
  "eslint-plugin-react": "7.29.4",
103
104
  "playwright": "1.20.2",
104
105
  "postcss-import": "14.1.0",
105
- "preact": "10.7.1",
106
106
  "prettier": "2.6.2",
107
- "react": "17.0.2",
108
- "react-dom": "17.0.2",
109
107
  "redux": "4.1.2",
110
108
  "rollup": "2.70.1"
111
109
  }
@@ -30,6 +30,7 @@ import {
30
30
  stringifyHtmlAst,
31
31
  } from "@jsenv/utils/html_ast/html_ast.js"
32
32
 
33
+ import { jsenvPluginUrlReferences } from "../plugins/url_references/jsenv_plugin_url_references.js"
33
34
  import { jsenvPluginInline } from "../plugins/inline/jsenv_plugin_inline.js"
34
35
  import { jsenvPluginAsJsClassic } from "../plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js"
35
36
  import { createUrlGraph } from "../omega/url_graph.js"
@@ -141,6 +142,10 @@ build ${entryPointKeys.length} entry points`)
141
142
  },
142
143
  },
143
144
  ...getCorePlugins({
145
+ rootDirectoryUrl,
146
+ urlGraph: rawGraph,
147
+ scenario: "build",
148
+
144
149
  nodeEsmResolution,
145
150
  fileSystemMagicResolution,
146
151
  injectedGlobals,
@@ -330,6 +335,7 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
330
335
  sourcemaps,
331
336
  runtimeCompat,
332
337
  plugins: [
338
+ jsenvPluginUrlReferences(),
333
339
  jsenvPluginAsJsClassic({
334
340
  systemJsInjection: true,
335
341
  }),
@@ -852,6 +858,7 @@ const applyUrlVersioning = async ({
852
858
  sourcemaps,
853
859
  runtimeCompat,
854
860
  plugins: [
861
+ jsenvPluginUrlReferences(),
855
862
  jsenvPluginInline({
856
863
  fetchInlineUrls: false,
857
864
  analyzeConvertedScripts: true, // to be able to version their urls
@@ -28,8 +28,9 @@ export const createBuilUrlsGenerator = ({ buildDirectoryUrl }) => {
28
28
  const urlObject = new URL(url)
29
29
  let { search, hash } = urlObject
30
30
  let name = getUrlName(url, urlInfo)
31
- const [basename, extension] = splitFileExtension(name)
32
- let nameCandidate = name
31
+ let [basename, extension] = splitFileExtension(name)
32
+ extension = extensionMappings[extension] || extension
33
+ let nameCandidate = `${basename}${extension}` // reconstruct name in case extension was normalized
33
34
  let integer = 1
34
35
  // eslint-disable-next-line no-constant-condition
35
36
  while (true) {
@@ -48,6 +49,18 @@ export const createBuilUrlsGenerator = ({ buildDirectoryUrl }) => {
48
49
  }
49
50
  }
50
51
 
52
+ // It's best to generate files with an extension representing what is inside the file
53
+ // and after build js files contains solely js (js or typescript is gone).
54
+ // This way a static file server is already configured to server the correct content-type
55
+ // (otherwise one would have to configure that ".jsx" is "text/javascript")
56
+ // To keep in mind: if you have "user.jsx" and "user.js" AND both file are not bundled
57
+ // you end up with "dist/js/user.js" and "dist/js/user2.js"
58
+ const extensionMappings = {
59
+ ".ts": ".js",
60
+ ".jsx": ".js",
61
+ ".tsx": ".js",
62
+ }
63
+
51
64
  const splitFileExtension = (filename) => {
52
65
  const dotLastIndex = filename.lastIndexOf(".")
53
66
  if (dotLastIndex === -1) {
@@ -21,6 +21,7 @@ import {
21
21
  pluginCORS,
22
22
  fetchFileSystem,
23
23
  } from "@jsenv/server"
24
+ import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
24
25
  import { createLogger } from "@jsenv/logger"
25
26
  import { Abort } from "@jsenv/abort"
26
27
 
@@ -50,6 +51,9 @@ export const startBuildServer = async ({
50
51
  cooldownBetweenFileEvents,
51
52
  autorestart,
52
53
  }) => {
54
+ rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl)
55
+ buildDirectoryUrl = assertAndNormalizeDirectoryUrl(buildDirectoryUrl)
56
+
53
57
  const autorestartProcess = await initProcessAutorestart({
54
58
  signal,
55
59
  handleSIGINT,
@@ -13,7 +13,6 @@ import { CONTENT_TYPE } from "@jsenv/utils/content_type/content_type.js"
13
13
  import { setUrlFilename } from "@jsenv/utils/urls/url_utils.js"
14
14
 
15
15
  import { createPluginController } from "../plugins/plugin_controller.js"
16
- import { jsenvPluginUrlReferences } from "../plugins/url_references/jsenv_plugin_url_references.js"
17
16
  import { createUrlInfoTransformer } from "./url_graph/url_info_transformations.js"
18
17
  import { RUNTIME_COMPAT } from "./compat/runtime_compat.js"
19
18
  import { defaultRuntimeCompat } from "./compat/default_runtime_compat.js"
@@ -50,7 +49,7 @@ export const createKitchen = ({
50
49
  writeOnFileSystem = true,
51
50
  }) => {
52
51
  const pluginController = createPluginController({
53
- plugins: [jsenvPluginUrlReferences(), ...plugins],
52
+ plugins,
54
53
  scenario,
55
54
  })
56
55
  const jsenvDirectoryUrl = new URL(".jsenv/", rootDirectoryUrl).href
@@ -119,6 +118,7 @@ export const createKitchen = ({
119
118
  // for inline ressources the reference contains the content
120
119
  content,
121
120
  contentType,
121
+ timing: {},
122
122
  }
123
123
  }
124
124
  const mutateReference = (reference, newReference) => {
@@ -208,6 +208,7 @@ export const createKitchen = ({
208
208
  })
209
209
  }
210
210
  }
211
+ baseContext.resolveReference = resolveReference
211
212
  const urlInfoTransformer = createUrlInfoTransformer({
212
213
  logger,
213
214
  urlGraph,
@@ -651,6 +652,7 @@ export const createKitchen = ({
651
652
 
652
653
  const applyReferenceEffectsOnUrlInfo = (reference, urlInfo, context) => {
653
654
  Object.assign(urlInfo.data, reference.data)
655
+ Object.assign(urlInfo.timing, reference.timing)
654
656
  if (reference.injected) {
655
657
  urlInfo.data.injected = true
656
658
  }
@@ -60,11 +60,11 @@ export const createFileService = ({
60
60
  [runtimeName]: runtimeVersion,
61
61
  },
62
62
  })
63
- const { response, contentType, content } = urlInfo
63
+ let { response, contentType, content } = urlInfo
64
64
  if (response) {
65
65
  return response
66
66
  }
67
- return {
67
+ response = {
68
68
  url: reference.url,
69
69
  status: 200,
70
70
  headers: {
@@ -73,7 +73,17 @@ export const createFileService = ({
73
73
  "cache-control": `private,max-age=0,must-revalidate`,
74
74
  },
75
75
  body: content,
76
+ timing: urlInfo.timing,
76
77
  }
78
+ kitchen.pluginController.callHooks(
79
+ "augmentResponse",
80
+ { reference, urlInfo },
81
+ {},
82
+ (returnValue) => {
83
+ response = composeTwoResponses(response, returnValue)
84
+ },
85
+ )
86
+ return response
77
87
  } catch (e) {
78
88
  const code = e.code
79
89
  if (code === "PARSE_ERROR") {
@@ -125,16 +135,7 @@ export const createFileService = ({
125
135
  }
126
136
  return async (request) => {
127
137
  let response = await getResponse(request)
128
- if (response.url) {
129
- kitchen.pluginController.callHooks(
130
- "augmentResponse",
131
- response,
132
- {},
133
- (returnValue) => {
134
- response = composeTwoResponses(response, returnValue)
135
- },
136
- )
137
- }
138
+
138
139
  return response
139
140
  }
140
141
  }
@@ -23,7 +23,12 @@ export const createUrlInfoTransformer = ({
23
23
  // for inline content (<script> insdide html)
24
24
  // chrome won't be able to fetch the file as it does not exists
25
25
  // so sourcemap must contain sources
26
- urlInfo.isInline || sourcemapsSources
26
+ sourcemapsSources ||
27
+ urlInfo.isInline ||
28
+ (sourcemap.sources &&
29
+ sourcemap.sources.some(
30
+ (source) => !source || !source.startsWith("file:"),
31
+ ))
27
32
  if (sourcemap.sources && sourcemap.sources.length > 1) {
28
33
  sourcemap.sources = sourcemap.sources.map(
29
34
  (source) => new URL(source, urlInfo.data.rawUrl || urlInfo.url).href,
@@ -150,5 +150,6 @@ const createUrlInfo = (url) => {
150
150
  content: undefined,
151
151
  sourcemap: null,
152
152
  sourcemapReference: null,
153
+ timing: {},
153
154
  }
154
155
  }
@@ -43,9 +43,8 @@ export const jsenvPluginDevSSEServer = ({
43
43
  urlInfo.data.hmrTimestamp = hmrTimestamp
44
44
  urlInfo.dependents.forEach((dependentUrl) => {
45
45
  const dependentUrlInfo = urlInfos[dependentUrl]
46
- if (
47
- !dependentUrlInfo.data.hotAcceptDependencies.includes(urlInfo.url)
48
- ) {
46
+ const { hotAcceptDependencies = [] } = dependentUrlInfo.data
47
+ if (!hotAcceptDependencies.includes(urlInfo.url)) {
49
48
  iterate(dependentUrlInfo, hmrTimestamp)
50
49
  }
51
50
  })
@@ -12,8 +12,14 @@ import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
12
12
 
13
13
  export const jsenvPluginCommonJsGlobals = () => {
14
14
  const transformCommonJsGlobals = async (urlInfo, { scenario }) => {
15
+ if (
16
+ !urlInfo.content.includes("process.env.NODE_ENV") &&
17
+ !urlInfo.content.includes("__filename") &&
18
+ !urlInfo.content.includes("__dirname")
19
+ ) {
20
+ return null
21
+ }
15
22
  const isJsModule = urlInfo.type === "js_module"
16
-
17
23
  const replaceMap = {
18
24
  "process.env.NODE_ENV": `("${
19
25
  scenario === "dev" || scenario === "test" ? "dev" : "prod"
@@ -6,7 +6,7 @@ import { urlToExtension } from "@jsenv/filesystem"
6
6
  import { applyFileSystemMagicResolution } from "@jsenv/node-esm-resolution"
7
7
 
8
8
  export const jsenvPluginFileSystemMagic = ({
9
- magicExtensions = ["inherit"],
9
+ magicExtensions = ["inherit", ".js"],
10
10
  magicDirectoryIndex = true,
11
11
  preservesSymlink = true,
12
12
  } = {}) => {
@@ -40,6 +40,9 @@ export const jsenvPluginImportMetaHot = () => {
40
40
  cssUrlInfo.data.hotAcceptDependencies = []
41
41
  },
42
42
  js_module: async (urlInfo, context) => {
43
+ if (!urlInfo.content.includes("import.meta.hot")) {
44
+ return null
45
+ }
43
46
  const { metadata } = await applyBabelPlugins({
44
47
  babelPlugins: [babelPluginMetadataImportMetaHot],
45
48
  urlInfo,
@@ -18,6 +18,13 @@ export const jsenvPluginImportMetaScenarios = () => {
18
18
  appliesDuring: "*",
19
19
  transformUrlContent: {
20
20
  js_module: async (urlInfo, { scenario }) => {
21
+ if (
22
+ !urlInfo.content.includes("import.meta.dev") &&
23
+ !urlInfo.content.includes("import.meta.test") &&
24
+ !urlInfo.content.includes("import.meta.build")
25
+ ) {
26
+ return null
27
+ }
21
28
  const { metadata } = await applyBabelPlugins({
22
29
  babelPlugins: [babelPluginMetadataImportMetaScenarios],
23
30
  urlInfo,
@@ -1,3 +1,9 @@
1
+ import {
2
+ parseHtmlString,
3
+ injectScriptAsEarlyAsPossible,
4
+ createHtmlNode,
5
+ stringifyHtmlAst,
6
+ } from "@jsenv/utils/html_ast/html_ast.js"
1
7
  import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
2
8
  import { isWebWorkerUrlInfo } from "@jsenv/core/src/omega/web_workers.js"
3
9
 
@@ -5,11 +11,41 @@ export const jsenvPluginInjectGlobals = (globals = {}) => {
5
11
  if (Object.keys(globals).length === 0) {
6
12
  return []
7
13
  }
8
- const injectGlobals = (urlInfo) => {
14
+
15
+ const globalInjectorOnHtmlEntryPoint = (urlInfo) => {
16
+ if (!urlInfo.data.isEntryPoint) {
17
+ return null
18
+ }
19
+ // ideally we would inject an importmap but browser support is too low
20
+ // (even worse for worker/service worker)
21
+ // so for now we inject code into entry points
22
+ const htmlAst = parseHtmlString(urlInfo.content, {
23
+ storeOriginalPositions: false,
24
+ })
25
+ injectScriptAsEarlyAsPossible(
26
+ htmlAst,
27
+ createHtmlNode({
28
+ "tagName": "script",
29
+ "textContent": generateClientCodeForGlobals({
30
+ globals,
31
+ isWebWorker: false,
32
+ }),
33
+ "injected-by": "jsenv:inject_globals",
34
+ }),
35
+ )
36
+ return stringifyHtmlAst(htmlAst)
37
+ }
38
+
39
+ const globalsInjectorOnJsEntryPoints = (urlInfo) => {
40
+ if (!urlInfo.data.isEntryPoint && !urlInfo.data.isWebWorkerEntryPoint) {
41
+ return null
42
+ }
9
43
  const magicSource = createMagicSource(urlInfo.content)
10
- const globalName = isWebWorkerUrlInfo(urlInfo) ? "self" : "window"
11
- magicSource.prepend(
12
- `Object.assign(${globalName}, ${JSON.stringify(globals, null, " ")});`,
44
+ magicSource.append(
45
+ generateClientCodeForGlobals({
46
+ globals,
47
+ isWebWorker: isWebWorkerUrlInfo(urlInfo),
48
+ }),
13
49
  )
14
50
  return magicSource.toContentAndSourcemap()
15
51
  }
@@ -18,8 +54,14 @@ export const jsenvPluginInjectGlobals = (globals = {}) => {
18
54
  name: "jsenv:inject_globals",
19
55
  appliesDuring: "*",
20
56
  transformUrlContent: {
21
- js_classic: injectGlobals,
22
- js_module: injectGlobals,
57
+ html: globalInjectorOnHtmlEntryPoint,
58
+ js_classic: globalsInjectorOnJsEntryPoints,
59
+ js_module: globalsInjectorOnJsEntryPoints,
23
60
  },
24
61
  }
25
62
  }
63
+
64
+ const generateClientCodeForGlobals = ({ isWebWorker = false, globals }) => {
65
+ const globalName = isWebWorker ? "self" : "window"
66
+ return `Object.assign(${globalName}, ${JSON.stringify(globals, null, " ")});`
67
+ }
@@ -7,6 +7,13 @@ import { generateInlineContentUrl } from "@jsenv/utils/urls/inline_content_url_g
7
7
 
8
8
  export const jsenvPluginJsInlineContent = ({ allowEscapeForVersioning }) => {
9
9
  const parseAndTransformInlineContentCalls = async (urlInfo, context) => {
10
+ if (
11
+ !urlInfo.content.includes("InlineContent(") &&
12
+ !urlInfo.content.includes("new Blob(") &&
13
+ !urlInfo.content.includes("JSON.parse(")
14
+ ) {
15
+ return null
16
+ }
10
17
  const { metadata } = await applyBabelPlugins({
11
18
  babelPlugins: [babelPluginMetadataInlineContentCalls],
12
19
  urlInfo,
@@ -7,25 +7,91 @@
7
7
  * it should likely be an other plugin happening after the others
8
8
  */
9
9
 
10
+ import { registerFileLifecycle } from "@jsenv/filesystem"
11
+
10
12
  import {
11
13
  applyNodeEsmResolution,
12
- lookupPackageScope,
13
- readPackageJson,
14
+ defaultLookupPackageScope,
15
+ defaultReadPackageJson,
14
16
  } from "@jsenv/node-esm-resolution"
15
17
 
16
18
  export const jsenvPluginNodeEsmResolution = ({
19
+ rootDirectoryUrl,
17
20
  // https://nodejs.org/api/esm.html#resolver-algorithm-specification
18
21
  packageConditions = ["browser", "import"],
19
- } = {}) => {
20
- const nodeEsmResolution = {
22
+ filesInvalidatingCache = ["package.json", "package-lock.json"],
23
+ }) => {
24
+ const packageScopesCache = new Map()
25
+ const lookupPackageScope = (url) => {
26
+ const fromCache = packageScopesCache.get(url)
27
+ if (fromCache) {
28
+ return fromCache
29
+ }
30
+ const packageScope = defaultLookupPackageScope(url)
31
+ packageScopesCache.set(url, packageScope)
32
+ return packageScope
33
+ }
34
+ const packageJsonsCache = new Map()
35
+ const readPackageJson = (url) => {
36
+ const fromCache = packageJsonsCache.get(url)
37
+ if (fromCache) {
38
+ return fromCache
39
+ }
40
+ const packageJson = defaultReadPackageJson(url)
41
+ packageJsonsCache.set(url, packageJson)
42
+ return packageJson
43
+ }
44
+
45
+ const unregisters = []
46
+ filesInvalidatingCache.forEach((file) => {
47
+ const unregister = registerFileLifecycle(new URL(file, rootDirectoryUrl), {
48
+ added: () => {
49
+ packageScopesCache.clear()
50
+ packageJsonsCache.clear()
51
+ },
52
+ updated: () => {
53
+ packageScopesCache.clear()
54
+ packageJsonsCache.clear()
55
+ },
56
+ removed: () => {
57
+ packageScopesCache.clear()
58
+ packageJsonsCache.clear()
59
+ },
60
+ keepProcessAlive: false,
61
+ })
62
+ unregisters.push(unregister)
63
+ })
64
+
65
+ return [
66
+ jsenvPluginNodeEsmResolver({
67
+ packageConditions,
68
+ lookupPackageScope,
69
+ readPackageJson,
70
+ }),
71
+ jsenvPluginNodeModulesVersionInUrls({
72
+ lookupPackageScope,
73
+ readPackageJson,
74
+ }),
75
+ ]
76
+ }
77
+
78
+ const jsenvPluginNodeEsmResolver = ({
79
+ packageConditions,
80
+ lookupPackageScope,
81
+ readPackageJson,
82
+ }) => {
83
+ return {
21
84
  name: "jsenv:node_esm_resolve",
22
85
  appliesDuring: "*",
23
86
  resolveUrl: {
24
87
  js_import_export: (reference) => {
88
+ const { parentUrl, specifier } = reference
25
89
  const { url } = applyNodeEsmResolution({
26
90
  conditions: packageConditions,
27
- parentUrl: reference.parentUrl,
28
- specifier: reference.specifier,
91
+ parentUrl,
92
+ specifier,
93
+ lookupPackageScope,
94
+ readPackageJson,
29
95
  })
30
96
  return url
31
97
  },
@@ -39,9 +105,14 @@ export const jsenvPluginNodeEsmResolution = ({
39
105
  return null
40
106
  },
41
107
  }
108
+ }
42
109
 
43
- const packageVersionInUrl = {
44
- name: "jsenv:package_url_version",
110
+ const jsenvPluginNodeModulesVersionInUrls = ({
111
+ lookupPackageScope,
112
+ readPackageJson,
113
+ }) => {
114
+ return {
115
+ name: "jsenv:node_modules_version_in_urls",
45
116
  appliesDuring: {
46
117
  dev: true,
47
118
  test: true,
@@ -72,6 +143,4 @@ export const jsenvPluginNodeEsmResolution = ({
72
143
  }
73
144
  },
74
145
  }
75
-
76
- return [nodeEsmResolution, packageVersionInUrl]
77
146
  }
@@ -1,3 +1,5 @@
1
+ import { timeStart } from "@jsenv/server"
2
+
1
3
  export const createPluginController = ({
2
4
  plugins,
3
5
  scenario,
@@ -47,7 +49,13 @@ export const createPluginController = ({
47
49
  }
48
50
  currentPlugin = hook.plugin
49
51
  currentHookName = hook.hookName
52
+ const timeEnd = timeStart(
53
+ `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
54
+ )
50
55
  let valueReturned = hookFn(info, context)
56
+ if (info.timing) {
57
+ Object.assign(info.timing, timeEnd())
58
+ }
51
59
  valueReturned = assertAndNormalizeReturnValue(hook.hookName, valueReturned)
52
60
  currentPlugin = null
53
61
  currentHookName = null
@@ -60,7 +68,13 @@ export const createPluginController = ({
60
68
  }
61
69
  currentPlugin = hook.plugin
62
70
  currentHookName = hook.hookName
71
+ const timeEnd = timeStart(
72
+ `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
73
+ )
63
74
  let valueReturned = await hookFn(info, context)
75
+ if (info.timing) {
76
+ Object.assign(info.timing, timeEnd())
77
+ }
64
78
  valueReturned = assertAndNormalizeReturnValue(hook.hookName, valueReturned)
65
79
  currentPlugin = null
66
80
  currentHookName = null
@@ -145,12 +159,18 @@ const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
145
159
  const { appliesDuring } = pluginEntry
146
160
  if (appliesDuring === undefined) {
147
161
  console.warn(`"appliesDuring" is undefined on ${pluginEntry.name}`)
162
+ return
148
163
  }
149
164
  if (appliesDuring === "*") {
150
165
  plugins.push(pluginEntry)
151
166
  return
152
167
  }
153
- if (appliesDuring && appliesDuring[scenario]) {
168
+ if (typeof appliesDuring !== "object") {
169
+ throw new Error(
170
+ `"appliesDuring" must be an object or "*", got ${appliesDuring}`,
171
+ )
172
+ }
173
+ if (appliesDuring[scenario]) {
154
174
  plugins.push(pluginEntry)
155
175
  return
156
176
  }
@@ -1,3 +1,4 @@
1
+ import { jsenvPluginUrlReferences } from "../plugins/url_references/jsenv_plugin_url_references.js"
1
2
  import { jsenvPluginLeadingSlash } from "./leading_slash/jsenv_plugin_leading_slash.js"
2
3
  import { jsenvPluginImportmap } from "./importmap/jsenv_plugin_importmap.js"
3
4
  import { jsenvPluginUrlResolution } from "./url_resolution/jsenv_plugin_url_resolution.js"
@@ -38,7 +39,11 @@ export const getCorePlugins = ({
38
39
  if (autoreload === true) {
39
40
  autoreload = {}
40
41
  }
42
+ if (nodeEsmResolution === true) {
43
+ nodeEsmResolution = {}
44
+ }
41
45
  return [
46
+ jsenvPluginUrlReferences(),
42
47
  jsenvPluginTranspilation(transpilation),
43
48
  ...(htmlSupervisor ? [jsenvPluginHtmlSupervisor(htmlSupervisor)] : []), // before inline as it turns inline <script> into <script src>
44
49
  jsenvPluginInline(), // before "file urls" to resolve and load inline urls
@@ -46,7 +51,11 @@ export const getCorePlugins = ({
46
51
  jsenvPluginFileUrls(),
47
52
  jsenvPluginHttpUrls(),
48
53
  jsenvPluginLeadingSlash(),
49
- jsenvPluginNodeEsmResolution(nodeEsmResolution), // before url resolution to handle "js_import_export" resolution
54
+ // before url resolution to handle "js_import_export" resolution
55
+ jsenvPluginNodeEsmResolution({
56
+ rootDirectoryUrl,
57
+ ...nodeEsmResolution,
58
+ }),
50
59
  jsenvPluginUrlResolution(),
51
60
  jsenvPluginFileSystemMagic(fileSystemMagicResolution),
52
61
  jsenvPluginUrlVersion(),
@@ -14,6 +14,10 @@ export const jsenvPluginImportAssertions = () => {
14
14
  appliesDuring: "*",
15
15
  transformUrlContent: {
16
16
  js_module: async (urlInfo, context) => {
17
+ // "usesImportAssertion" is set by "jsenv:imports_analysis"
18
+ if (!urlInfo.data.usesImportAssertion) {
19
+ return null
20
+ }
17
21
  const importTypesToTranspile = getImportTypesToTranspile(context)
18
22
  if (importTypesToTranspile.length === 0) {
19
23
  return null
@@ -8,10 +8,11 @@ export const jsenvPluginTopLevelAwait = () => {
8
8
  appliesDuring: "*",
9
9
  transformUrlContent: {
10
10
  js_module: async (urlInfo, context) => {
11
- if (!urlInfo.data.usesTopLevelAwait) {
11
+ if (context.isSupportedOnCurrentClients("top_level_await")) {
12
12
  return null
13
13
  }
14
- if (context.isSupportedOnCurrentClients("top_level_await")) {
14
+ const usesTLA = await usesTopLevelAwait(urlInfo)
15
+ if (!usesTLA) {
15
16
  return null
16
17
  }
17
18
  const { code, map } = await applyBabelPlugins({
@@ -35,3 +36,35 @@ export const jsenvPluginTopLevelAwait = () => {
35
36
  },
36
37
  }
37
38
  }
39
+
40
+ const usesTopLevelAwait = async (urlInfo) => {
41
+ if (!urlInfo.content.includes("await ")) {
42
+ return false
43
+ }
44
+ const { metadata } = await applyBabelPlugins({
45
+ urlInfo,
46
+ babelPlugins: [babelPluginMetadataUsesTopLevelAwait],
47
+ })
48
+ return metadata.usesTopLevelAwait
49
+ }
50
+
51
+ const babelPluginMetadataUsesTopLevelAwait = () => {
52
+ return {
53
+ name: "metadata-uses-top-level-await",
54
+ visitor: {
55
+ Program: (programPath, state) => {
56
+ let usesTopLevelAwait = false
57
+ programPath.traverse({
58
+ AwaitExpression: (path) => {
59
+ const closestFunction = path.getFunctionParent()
60
+ if (!closestFunction) {
61
+ usesTopLevelAwait = true
62
+ path.stop()
63
+ }
64
+ },
65
+ })
66
+ state.file.metadata.usesTopLevelAwait = usesTopLevelAwait
67
+ },
68
+ },
69
+ }
70
+ }
@@ -1,8 +1,5 @@
1
1
  import { applyBabelPlugins } from "@jsenv/utils/js_ast/apply_babel_plugins.js"
2
2
  import {
3
- analyzeImportCall,
4
- isImportCall,
5
- analyzeImportExportDeclaration,
6
3
  analyzeNewUrlCall,
7
4
  analyzeNewWorkerOrNewSharedWorker,
8
5
  analyzeImportScriptCalls,
@@ -14,19 +11,7 @@ import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
14
11
  import { isWebWorkerUrlInfo } from "@jsenv/core/src/omega/web_workers.js"
15
12
 
16
13
  export const parseAndTransformJsUrls = async (urlInfo, context) => {
17
- const isJsModule = urlInfo.type === "js_module"
18
- const isWebWorker = isWebWorkerUrlInfo(urlInfo)
19
- const { metadata } = await applyBabelPlugins({
20
- babelPlugins: [
21
- [babelPluginMetadataJsUrlMentions, { isJsModule, isWebWorker }],
22
- ],
23
- urlInfo,
24
- })
25
- const { jsMentions, usesTopLevelAwait, usesImport, usesExport } = metadata
26
- urlInfo.data.usesImport = usesImport
27
- urlInfo.data.usesExport = usesExport
28
- urlInfo.data.usesTopLevelAwait = usesTopLevelAwait
29
-
14
+ const jsMentions = await performJsUrlsStaticAnalysis(urlInfo)
30
15
  const { rootDirectoryUrl, referenceUtils } = context
31
16
  const actions = []
32
17
  const magicSource = createMagicSource(urlInfo.content)
@@ -58,6 +43,51 @@ export const parseAndTransformJsUrls = async (urlInfo, context) => {
58
43
  return magicSource.toContentAndSourcemap()
59
44
  }
60
45
 
46
+ const performJsUrlsStaticAnalysis = async (urlInfo) => {
47
+ const isJsModule = urlInfo.type === "js_module"
48
+ const isWebWorker = isWebWorkerUrlInfo(urlInfo)
49
+ if (canSkipStaticAnalysis(urlInfo, { isJsModule, isWebWorker })) {
50
+ return []
51
+ }
52
+ const { metadata } = await applyBabelPlugins({
53
+ babelPlugins: [
54
+ [babelPluginMetadataJsUrlMentions, { isJsModule, isWebWorker }],
55
+ ],
56
+ urlInfo,
57
+ })
58
+ const { jsMentions } = metadata
59
+ return jsMentions
60
+ }
61
+
62
+ const canSkipStaticAnalysis = (urlInfo, { isJsModule, isWebWorker }) => {
63
+ const js = urlInfo.content
64
+ if (isJsModule) {
65
+ if (
66
+ js.includes("new URL(") ||
67
+ js.includes("new Worker(") ||
68
+ js.includes("new SharedWorker(") ||
69
+ js.includes("serviceWorker.register(")
70
+ ) {
71
+ return false
72
+ }
73
+ }
74
+ if (!isJsModule) {
75
+ if (
76
+ js.includes("System.") ||
77
+ js.includes("new URL(") ||
78
+ js.includes("new Worker(") ||
79
+ js.includes("new SharedWorker(") ||
80
+ js.includes("serviceWorker.register(")
81
+ ) {
82
+ return false
83
+ }
84
+ }
85
+ if (isWebWorker && js.includes("importScripts(")) {
86
+ return false
87
+ }
88
+ return true
89
+ }
90
+
61
91
  /*
62
92
  * see also
63
93
  * https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
@@ -70,10 +100,6 @@ const babelPluginMetadataJsUrlMentions = (_, { isJsModule, isWebWorker }) => {
70
100
  visitor: {
71
101
  Program(programPath, state) {
72
102
  const jsMentions = []
73
- let usesImport = false
74
- let usesExport = false
75
- let usesTopLevelAwait = false
76
-
77
103
  const callOneStaticAnalyzer = (path, analyzer) => {
78
104
  const returnValue = analyzer(path)
79
105
  if (returnValue === null) {
@@ -96,14 +122,7 @@ const babelPluginMetadataJsUrlMentions = (_, { isJsModule, isWebWorker }) => {
96
122
  }
97
123
  }
98
124
  }
99
-
100
125
  const visitors = {
101
- AwaitExpression: (path) => {
102
- const closestFunction = path.getFunctionParent()
103
- if (!closestFunction) {
104
- usesTopLevelAwait = true
105
- }
106
- },
107
126
  NewExpression: (path) => {
108
127
  callStaticAnalyzers(path, [
109
128
  (path) => analyzeNewWorkerOrNewSharedWorker(path, { isJsModule }),
@@ -113,50 +132,16 @@ const babelPluginMetadataJsUrlMentions = (_, { isJsModule, isWebWorker }) => {
113
132
  }
114
133
  const callExpressionStaticAnalysers = [
115
134
  ...(isJsModule
116
- ? [analyzeImportCall]
135
+ ? []
117
136
  : [analyzeSystemRegisterCall, analyzeSystemImportCall]),
118
137
  ...(isWebWorker ? [analyzeImportScriptCalls] : []),
119
138
  analyzeServiceWorkerRegisterCall,
120
139
  ]
121
140
  visitors.CallExpression = (path) => {
122
- if (isJsModule && !usesImport && isImportCall(path.node)) {
123
- usesImport = true
124
- }
125
141
  callStaticAnalyzers(path, callExpressionStaticAnalysers)
126
142
  }
127
-
128
- if (isJsModule) {
129
- Object.assign(visitors, {
130
- ExportNamedDeclaration: (path) => {
131
- if (!usesImport && path.node.source) {
132
- usesImport = true
133
- }
134
- usesExport = true
135
- callStaticAnalyzers(path, [analyzeImportExportDeclaration])
136
- },
137
- ExportAllDeclaration: (path) => {
138
- usesImport = true
139
- usesExport = true
140
- callStaticAnalyzers(path, [analyzeImportExportDeclaration])
141
- },
142
- ExportDefaultDeclaration: (path) => {
143
- if (!usesImport && path.node.source) {
144
- usesImport = true
145
- }
146
- usesExport = true
147
- callStaticAnalyzers(path, [analyzeImportExportDeclaration])
148
- },
149
- ImportDeclaration: (path) => {
150
- usesImport = true
151
- callStaticAnalyzers(path, [analyzeImportExportDeclaration])
152
- },
153
- })
154
- }
155
143
  programPath.traverse(visitors)
156
144
  state.file.metadata.jsMentions = jsMentions
157
- state.file.metadata.usesImport = usesImport
158
- state.file.metadata.usesExport = usesExport
159
- state.file.metadata.usesTopLevelAwait = usesTopLevelAwait
160
145
  },
161
146
  },
162
147
  }
@@ -0,0 +1,46 @@
1
+ import { parseJsModuleImports } from "@jsenv/utils/js_ast/parse_js_module_imports.js"
2
+ import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
3
+
4
+ export const jsenvPluginImportsAnalysis = () => {
5
+ return {
6
+ name: "jsenv:imports_analysis",
7
+ appliesDuring: "*",
8
+ transformUrlContent: {
9
+ js_module: async (urlInfo, context) => {
10
+ const [imports, exports] = await parseJsModuleImports(
11
+ urlInfo.content,
12
+ (urlInfo.data && urlInfo.data.rawUrl) || urlInfo.url,
13
+ )
14
+ const actions = []
15
+ const magicSource = createMagicSource(urlInfo.content)
16
+ urlInfo.data.usesImport = imports.length > 0
17
+ urlInfo.data.usesExport = exports.length > 0
18
+ urlInfo.data.usesImportAssertion = imports.some(
19
+ (importInfo) => importInfo.usesAssert,
20
+ )
21
+ imports.forEach((importInfo) => {
22
+ const [reference] = context.referenceUtils.found({
23
+ type: "js_import_export",
24
+ subtype: importInfo.subtype,
25
+ expectedType: "js_module",
26
+ expectedSubtype: urlInfo.subtype,
27
+ line: importInfo.line,
28
+ column: importInfo.column,
29
+ specifier: importInfo.specifier,
30
+ })
31
+ actions.push(async () => {
32
+ magicSource.replace({
33
+ start: importInfo.start,
34
+ end: importInfo.end,
35
+ replacement: await context.referenceUtils.readGeneratedSpecifier(
36
+ reference,
37
+ ),
38
+ })
39
+ })
40
+ })
41
+ await Promise.all(actions.map((action) => action()))
42
+ return magicSource.toContentAndSourcemap()
43
+ },
44
+ },
45
+ }
46
+ }
@@ -0,0 +1,18 @@
1
+ import { parseAndTransformHtmlUrls } from "./html/html_urls.js"
2
+ import { parseAndTransformCssUrls } from "./css/css_urls.js"
3
+ import { parseAndTransformJsUrls } from "./js/js_urls.js"
4
+ import { parseAndTransformWebmanifestUrls } from "./webmanifest/webmanifest_urls.js"
5
+
6
+ export const jsenvPluginUrlAnalysis = () => {
7
+ return {
8
+ name: "jsenv:url_analysis",
9
+ appliesDuring: "*",
10
+ transformUrlContent: {
11
+ html: parseAndTransformHtmlUrls,
12
+ css: parseAndTransformCssUrls,
13
+ js_classic: parseAndTransformJsUrls,
14
+ js_module: parseAndTransformJsUrls,
15
+ webmanifest: parseAndTransformWebmanifestUrls,
16
+ },
17
+ }
18
+ }
@@ -1,18 +1,6 @@
1
- import { parseAndTransformHtmlUrls } from "./html/html_urls.js"
2
- import { parseAndTransformCssUrls } from "./css/css_urls.js"
3
- import { parseAndTransformJsUrls } from "./js/js_urls.js"
4
- import { parseAndTransformWebmanifestUrls } from "./webmanifest/webmanifest_urls.js"
1
+ import { jsenvPluginImportsAnalysis } from "./jsenv_plugin_imports_analysis.js"
2
+ import { jsenvPluginUrlAnalysis } from "./jsenv_plugin_url_analysis.js"
5
3
 
6
4
  export const jsenvPluginUrlReferences = () => {
7
- return {
8
- name: "jsenv:url_references",
9
- appliesDuring: "*",
10
- transformUrlContent: {
11
- html: parseAndTransformHtmlUrls,
12
- css: parseAndTransformCssUrls,
13
- js_classic: parseAndTransformJsUrls,
14
- js_module: parseAndTransformJsUrls,
15
- webmanifest: parseAndTransformWebmanifestUrls,
16
- },
17
- }
5
+ return [jsenvPluginImportsAnalysis(), jsenvPluginUrlAnalysis()]
18
6
  }
@@ -1,6 +1,4 @@
1
- export const jsenvPluginUrlVersion = ({
2
- longTermCache = false, // will become true once things get more stable
3
- } = {}) => {
1
+ export const jsenvPluginUrlVersion = ({ longTermCache = true } = {}) => {
4
2
  return {
5
3
  name: "jsenv:url_version",
6
4
  appliesDuring: "*", // maybe only during dev?
@@ -15,7 +13,7 @@ export const jsenvPluginUrlVersion = ({
15
13
  urlObject.searchParams.delete("v")
16
14
  return urlObject.href
17
15
  },
18
- transformUrl: (reference) => {
16
+ transformUrlSearchParams: (reference) => {
19
17
  if (!reference.data.version) {
20
18
  return null
21
19
  }
@@ -26,15 +24,14 @@ export const jsenvPluginUrlVersion = ({
26
24
  v: reference.data.version,
27
25
  }
28
26
  },
29
- augmentResponse: ({ url }) => {
27
+ augmentResponse: ({ reference }) => {
30
28
  if (!longTermCache) {
31
29
  return null
32
30
  }
33
- const urlObject = new URL(url)
34
- if (!urlObject.searchParams.has("v")) {
31
+ if (!reference.searchParams.has("v")) {
35
32
  return null
36
33
  }
37
- if (urlObject.searchParams.has("hmr")) {
34
+ if (reference.searchParams.has("hmr")) {
38
35
  return null
39
36
  }
40
37
  // When url is versioned put it in browser cache for 30 days
@@ -142,7 +142,10 @@ export const executePlan = async (
142
142
  plugins: [
143
143
  ...plugins,
144
144
  ...getCorePlugins({
145
+ rootDirectoryUrl,
146
+ urlGraph,
145
147
  scenario,
148
+
146
149
  htmlSupervisor: true,
147
150
  nodeEsmResolution,
148
151
  fileSystemMagicResolution,
@@ -13,6 +13,14 @@ import { generateCoverageHtmlDirectory } from "@jsenv/utils/coverage/coverage_re
13
13
  import { generateCoverageTextLog } from "@jsenv/utils/coverage/coverage_reporter_text_log.js"
14
14
  import { executePlan } from "./execute_plan.js"
15
15
 
16
+ export const defaultCoverageConfig = {
17
+ "./index.js": true,
18
+ "./main.js": true,
19
+ "./src/**/*.js": true,
20
+ "./**/*.test.*": false, // contains .test. -> nope
21
+ "./**/test/": false, // inside a test folder -> nope,
22
+ }
23
+
16
24
  /**
17
25
  * Execute a list of files and log how it goes
18
26
  * @param {Object} testPlanParameters
@@ -57,13 +65,7 @@ export const executeTestPlan = async ({
57
65
  coverage = process.argv.includes("--cover") ||
58
66
  process.argv.includes("--coverage"),
59
67
  coverageTempDirectoryRelativeUrl = "./.coverage/tmp/",
60
- coverageConfig = {
61
- "./index.js": true,
62
- "./main.js": true,
63
- "./src/**/*.js": true,
64
- "./**/*.test.*": false, // contains .test. -> nope
65
- "./**/test/": false, // inside a test folder -> nope,
66
- },
68
+ coverageConfig = defaultCoverageConfig,
67
69
  coverageIncludeMissing = true,
68
70
  coverageAndExecutionAllowed = false,
69
71
  coverageForceIstanbul = false,