@jsenv/core 28.5.0 → 29.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.
@@ -11,7 +11,8 @@ export const jsenvPluginUrlAnalysis = ({
11
11
  include,
12
12
  supportedProtocols = ["file:", "data:", "virtual:", "http:", "https:"],
13
13
  }) => {
14
- let getIncludeInfo = () => undefined
14
+ // eslint-disable-next-line no-unused-vars
15
+ let getIncludeInfo = (url) => undefined
15
16
  if (include) {
16
17
  const associations = URL_META.resolveAssociations(
17
18
  { include },
@@ -56,7 +57,6 @@ export const jsenvPluginUrlAnalysis = ({
56
57
  )
57
58
  if (protocolIsSupported) {
58
59
  reference.shouldHandle = true
59
- return
60
60
  }
61
61
  },
62
62
  transformUrlContent: {
@@ -1,34 +1,115 @@
1
- export const jsenvPluginUrlResolution = () => {
2
- const urlResolver = (reference) => {
1
+ /*
2
+ * This plugin is responsible to resolve urls except for a few cases:
3
+ * - A custom plugin implements a resolveUrl hook returning something
4
+ * - The reference.type is "filesystem" -> it is handled by jsenv_plugin_file_urls.js
5
+ *
6
+ * By default node esm resolution applies inside js modules
7
+ * and the rest uses the web standard url resolution (new URL):
8
+ * - "http_request"
9
+ * - "entry_point"
10
+ * - "js_import_export"
11
+ * - "link_href"
12
+ * - "script_src"
13
+ * - "a_href"
14
+ * - "iframe_src
15
+ * - "img_src"
16
+ * - "img_srcset"
17
+ * - "source_src"
18
+ * - "source_srcset"
19
+ * - "image_href"
20
+ * - "use_href"
21
+ * - "css_@import"
22
+ * - "css_url"
23
+ * - "sourcemap_comment"
24
+ * - "js_url_specifier"
25
+ * - "js_inline_content"
26
+ * - "webmanifest_icon_src"
27
+ * - "package_json"
28
+ */
29
+
30
+ import { createNodeEsmResolver } from "./node_esm_resolver.js"
31
+
32
+ export const jsenvPluginUrlResolution = ({
33
+ runtimeCompat,
34
+ clientMainFileUrl,
35
+ urlResolution,
36
+ }) => {
37
+ const resolveUrlUsingWebResolution = (reference) => {
3
38
  return new URL(
4
39
  reference.specifier,
5
40
  reference.baseUrl || reference.parentUrl,
6
41
  ).href
7
42
  }
43
+
44
+ const resolvers = {}
45
+ Object.keys(urlResolution).forEach((referenceType) => {
46
+ const resolver = urlResolution[referenceType]
47
+ if (typeof resolver !== "object") {
48
+ throw new Error(
49
+ `Unexpected urlResolution configuration:
50
+ "${referenceType}" resolution value must be an object, got ${resolver}`,
51
+ )
52
+ }
53
+ let { web, node_esm, ...rest } = resolver
54
+ const unexpectedKey = Object.keys(rest)[0]
55
+ if (unexpectedKey) {
56
+ throw new Error(
57
+ `Unexpected urlResolution configuration:
58
+ "${referenceType}" resolution key must be "web" or "node_esm", found "${
59
+ Object.keys(rest)[0]
60
+ }"`,
61
+ )
62
+ }
63
+ if (node_esm === undefined) {
64
+ node_esm = referenceType === "js_import_export"
65
+ }
66
+ if (web === undefined) {
67
+ web = true
68
+ }
69
+ if (node_esm) {
70
+ if (node_esm === true) node_esm = {}
71
+ const { packageConditions } = node_esm
72
+ resolvers[referenceType] = createNodeEsmResolver({
73
+ runtimeCompat,
74
+ packageConditions,
75
+ })
76
+ } else if (web) {
77
+ resolvers[referenceType] = resolveUrlUsingWebResolution
78
+ }
79
+ })
80
+
81
+ if (!resolvers["js_import_export"]) {
82
+ resolvers.js_import_export = createNodeEsmResolver({ runtimeCompat })
83
+ }
84
+ if (!resolvers["*"]) {
85
+ resolvers["*"] = resolveUrlUsingWebResolution
86
+ }
87
+
8
88
  return {
9
89
  name: "jsenv:url_resolution",
10
90
  appliesDuring: "*",
11
- resolveUrl: {
12
- "http_request": urlResolver, // during dev
13
- "entry_point": urlResolver, // during build
14
- "link_href": urlResolver,
15
- "script_src": urlResolver,
16
- "a_href": urlResolver,
17
- "iframe_src": urlResolver,
18
- "img_src": urlResolver,
19
- "img_srcset": urlResolver,
20
- "source_src": urlResolver,
21
- "source_srcset": urlResolver,
22
- "image_href": urlResolver,
23
- "use_href": urlResolver,
24
- "css_@import": urlResolver,
25
- "css_url": urlResolver,
26
- "sourcemap_comment": urlResolver,
27
- "js_import_export": urlResolver,
28
- "js_url_specifier": urlResolver,
29
- "js_inline_content": urlResolver,
30
- "webmanifest_icon_src": urlResolver,
31
- "package_json": urlResolver,
91
+ resolveUrl: (reference, context) => {
92
+ if (reference.specifier === "/") {
93
+ return String(clientMainFileUrl)
94
+ }
95
+ if (reference.specifier[0] === "/") {
96
+ return new URL(reference.specifier.slice(1), context.rootDirectoryUrl)
97
+ .href
98
+ }
99
+ const resolver = resolvers[reference.type] || resolvers["*"]
100
+ return resolver(reference, context)
101
+ },
102
+ // when specifier is prefixed by "file:///@ignore/"
103
+ // we return an empty js module (used by node esm)
104
+ fetchUrlContent: (urlInfo) => {
105
+ if (urlInfo.url.startsWith("file:///@ignore/")) {
106
+ return {
107
+ content: "export default {}",
108
+ contentType: "text/javascript",
109
+ type: "js_module",
110
+ }
111
+ }
112
+ return null
32
113
  },
33
114
  }
34
115
  }
@@ -0,0 +1,110 @@
1
+ /*
2
+ * - should I restore eventual search params lost during node esm resolution
3
+ * - what about symlinks?
4
+ * It feels like I should apply symlink (when we don't want to preserve them)
5
+ * once a file:/// url is found, regardless
6
+ * if that comes from node resolution or anything else (not even magic resolution)
7
+ * it should likely be an other plugin happening after the others
8
+ */
9
+
10
+ import { readFileSync } from "node:fs"
11
+ import { bufferToEtag } from "@jsenv/filesystem"
12
+ import {
13
+ applyNodeEsmResolution,
14
+ readCustomConditionsFromProcessArgs,
15
+ defaultLookupPackageScope,
16
+ defaultReadPackageJson,
17
+ } from "@jsenv/node-esm-resolution"
18
+
19
+ export const createNodeEsmResolver = ({ runtimeCompat, packageConditions }) => {
20
+ const nodeRuntimeEnabled = Object.keys(runtimeCompat).includes("node")
21
+ // https://nodejs.org/api/esm.html#resolver-algorithm-specification
22
+ packageConditions = packageConditions || [
23
+ ...readCustomConditionsFromProcessArgs(),
24
+ nodeRuntimeEnabled ? "node" : "browser",
25
+ "import",
26
+ ]
27
+
28
+ return (reference, context) => {
29
+ const { parentUrl, specifier } = reference
30
+ const { url, type, packageUrl } = applyNodeEsmResolution({
31
+ conditions: packageConditions,
32
+ parentUrl,
33
+ specifier,
34
+ })
35
+ if (context.scenarios.dev) {
36
+ const dependsOnPackageJson =
37
+ type !== "relative_specifier" &&
38
+ type !== "absolute_specifier" &&
39
+ type !== "node_builtin_specifier"
40
+ if (dependsOnPackageJson) {
41
+ // this reference depends on package.json and node_modules
42
+ // to be resolved. Each file using this specifier
43
+ // must be invalidated when corresponding package.json changes
44
+ addRelationshipWithPackageJson({
45
+ reference,
46
+ context,
47
+ packageJsonUrl: `${packageUrl}package.json`,
48
+ field: type.startsWith("field:")
49
+ ? `#${type.slice("field:".length)}`
50
+ : "",
51
+ })
52
+ }
53
+ }
54
+ if (context.scenarios.dev) {
55
+ // without this check a file inside a project without package.json
56
+ // could be considered as a node module if there is a ancestor package.json
57
+ // but we want to version only node modules
58
+ if (url.includes("/node_modules/")) {
59
+ const packageDirectoryUrl = defaultLookupPackageScope(url)
60
+ if (
61
+ packageDirectoryUrl &&
62
+ packageDirectoryUrl !== context.rootDirectoryUrl
63
+ ) {
64
+ const packageVersion =
65
+ defaultReadPackageJson(packageDirectoryUrl).version
66
+ // package version can be null, see https://github.com/babel/babel/blob/2ce56e832c2dd7a7ed92c89028ba929f874c2f5c/packages/babel-runtime/helpers/esm/package.json#L2
67
+ if (packageVersion) {
68
+ addRelationshipWithPackageJson({
69
+ reference,
70
+ context,
71
+ packageJsonUrl: `${packageDirectoryUrl}package.json`,
72
+ field: "version",
73
+ hasVersioningEffect: true,
74
+ })
75
+ }
76
+ reference.version = packageVersion
77
+ }
78
+ }
79
+ }
80
+ return url
81
+ }
82
+ }
83
+
84
+ const addRelationshipWithPackageJson = ({
85
+ context,
86
+ packageJsonUrl,
87
+ field,
88
+ hasVersioningEffect = false,
89
+ }) => {
90
+ const referenceFound = context.referenceUtils.find(
91
+ (ref) => ref.type === "package_json" && ref.subtype === field,
92
+ )
93
+ if (referenceFound) {
94
+ return
95
+ }
96
+ const [, packageJsonUrlInfo] = context.referenceUtils.inject({
97
+ type: "package_json",
98
+ subtype: field,
99
+ specifier: packageJsonUrl,
100
+ isImplicit: true,
101
+ hasVersioningEffect,
102
+ })
103
+ if (packageJsonUrlInfo.type === undefined) {
104
+ const packageJsonContentAsBuffer = readFileSync(new URL(packageJsonUrl))
105
+ packageJsonUrlInfo.type = "json"
106
+ packageJsonUrlInfo.content = String(packageJsonContentAsBuffer)
107
+ packageJsonUrlInfo.originalContentEtag = packageJsonUrlInfo.contentEtag =
108
+ bufferToEtag(packageJsonContentAsBuffer)
109
+ }
110
+ }
@@ -9,19 +9,24 @@ export const jsenvPluginUrlVersion = () => {
9
9
  // this goal is achieved when we reach this part of the code
10
10
  // We get rid of this params so that urlGraph and other parts of the code
11
11
  // recognize the url (it is not considered as a different url)
12
- const urlObject = new URL(reference.url)
13
- urlObject.searchParams.delete("v")
14
- return urlObject.href
12
+ const version = reference.searchParams.get("v")
13
+ if (version) {
14
+ const urlObject = new URL(reference.url)
15
+ urlObject.searchParams.delete("v")
16
+ reference.version = version
17
+ return urlObject.href
18
+ }
19
+ return null
15
20
  },
16
21
  transformUrlSearchParams: (reference) => {
17
- if (!reference.data.version) {
22
+ if (!reference.version) {
18
23
  return null
19
24
  }
20
25
  if (reference.searchParams.has("v")) {
21
26
  return null
22
27
  }
23
28
  return {
24
- v: reference.data.version,
29
+ v: reference.version,
25
30
  }
26
31
  },
27
32
  }
@@ -1,13 +0,0 @@
1
- export const jsenvPluginLeadingSlash = () => {
2
- return {
3
- name: "jsenv:leading_slash",
4
- appliesDuring: "*",
5
- resolveUrl: (reference, context) => {
6
- if (reference.specifier[0] !== "/") {
7
- return null
8
- }
9
- return new URL(reference.specifier.slice(1), context.rootDirectoryUrl)
10
- .href
11
- },
12
- }
13
- }
@@ -1,148 +0,0 @@
1
- /*
2
- * - should I restore eventual search params lost during node esm resolution
3
- * - what about symlinks?
4
- * It feels like I should apply symlink (when we don't want to preserve them)
5
- * once a file:/// url is found, regardless
6
- * if that comes from node resolution or anything else (not even magic resolution)
7
- * it should likely be an other plugin happening after the others
8
- */
9
-
10
- import { readFileSync } from "node:fs"
11
- import { bufferToEtag } from "@jsenv/filesystem"
12
- import {
13
- applyNodeEsmResolution,
14
- defaultLookupPackageScope,
15
- defaultReadPackageJson,
16
- readCustomConditionsFromProcessArgs,
17
- } from "@jsenv/node-esm-resolution"
18
-
19
- export const jsenvPluginNodeEsmResolution = ({ packageConditions }) => {
20
- const addRelationshipWithPackageJson = ({
21
- context,
22
- packageJsonUrl,
23
- field,
24
- hasVersioningEffect = false,
25
- }) => {
26
- const referenceFound = context.referenceUtils.find(
27
- (ref) => ref.type === "package_json" && ref.subtype === field,
28
- )
29
- if (referenceFound) {
30
- return
31
- }
32
- const [, packageJsonUrlInfo] = context.referenceUtils.inject({
33
- type: "package_json",
34
- subtype: field,
35
- specifier: packageJsonUrl,
36
- isImplicit: true,
37
- hasVersioningEffect,
38
- })
39
- if (packageJsonUrlInfo.type === undefined) {
40
- const packageJsonContentAsBuffer = readFileSync(new URL(packageJsonUrl))
41
- packageJsonUrlInfo.type = "json"
42
- packageJsonUrlInfo.content = String(packageJsonContentAsBuffer)
43
- packageJsonUrlInfo.originalContentEtag = packageJsonUrlInfo.contentEtag =
44
- bufferToEtag(packageJsonContentAsBuffer)
45
- }
46
- }
47
-
48
- return {
49
- name: "jsenv:node_esm_resolution",
50
- appliesDuring: "*",
51
- init: (context) => {
52
- const nodeRuntimeEnabled = Object.keys(context.runtimeCompat).includes(
53
- "node",
54
- )
55
- // https://nodejs.org/api/esm.html#resolver-algorithm-specification
56
- packageConditions = packageConditions || [
57
- ...readCustomConditionsFromProcessArgs(),
58
- nodeRuntimeEnabled ? "node" : "browser",
59
- "import",
60
- ]
61
- },
62
- resolveUrl: {
63
- js_import_export: (reference, context) => {
64
- const { parentUrl, specifier } = reference
65
- const { url, type, packageUrl } = applyNodeEsmResolution({
66
- conditions: packageConditions,
67
- parentUrl,
68
- specifier,
69
- })
70
- if (context.scenarios.dev) {
71
- const dependsOnPackageJson =
72
- type !== "relative_specifier" &&
73
- type !== "absolute_specifier" &&
74
- type !== "node_builtin_specifier"
75
- if (dependsOnPackageJson) {
76
- // this reference depends on package.json and node_modules
77
- // to be resolved. Each file using this specifier
78
- // must be invalidated when corresponding package.json changes
79
- addRelationshipWithPackageJson({
80
- reference,
81
- context,
82
- packageJsonUrl: `${packageUrl}package.json`,
83
- field: type.startsWith("field:")
84
- ? `#${type.slice("field:".length)}`
85
- : "",
86
- })
87
- }
88
- }
89
- return url
90
- },
91
- },
92
- transformUrlSearchParams: (reference, context) => {
93
- if (reference.type === "package_json") {
94
- return null
95
- }
96
- if (context.scenarios.build) {
97
- return null
98
- }
99
- if (!reference.url.startsWith("file:")) {
100
- return null
101
- }
102
- // without this check a file inside a project without package.json
103
- // could be considered as a node module if there is a ancestor package.json
104
- // but we want to version only node modules
105
- if (!reference.url.includes("/node_modules/")) {
106
- return null
107
- }
108
- if (reference.searchParams.has("v")) {
109
- return null
110
- }
111
- const packageDirectoryUrl = defaultLookupPackageScope(reference.url)
112
- if (!packageDirectoryUrl) {
113
- return null
114
- }
115
- if (packageDirectoryUrl === context.rootDirectoryUrl) {
116
- return null
117
- }
118
- // there is a dependency between this file and the package.json version field
119
- const packageVersion = defaultReadPackageJson(packageDirectoryUrl).version
120
- if (!packageVersion) {
121
- // example where it happens: https://github.com/babel/babel/blob/2ce56e832c2dd7a7ed92c89028ba929f874c2f5c/packages/babel-runtime/helpers/esm/package.json#L2
122
- return null
123
- }
124
- if (reference.type === "js_import_export") {
125
- addRelationshipWithPackageJson({
126
- reference,
127
- context,
128
- packageJsonUrl: `${packageDirectoryUrl}package.json`,
129
- field: "version",
130
- hasVersioningEffect: true,
131
- })
132
- }
133
- return {
134
- v: packageVersion,
135
- }
136
- },
137
- fetchUrlContent: (urlInfo) => {
138
- if (urlInfo.url.startsWith("file:///@ignore/")) {
139
- return {
140
- content: "export default {}",
141
- contentType: "text/javascript",
142
- type: "js_module",
143
- }
144
- }
145
- return null
146
- },
147
- }
148
- }