@flex-development/mlly 1.0.0-alpha.11 → 1.0.0-alpha.13

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 (95) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/dist/interfaces/options-get-format.d.mts +3 -13
  4. package/dist/interfaces/options-get-source.d.mts +3 -13
  5. package/dist/interfaces/options-parse-subpath.d.mts +18 -0
  6. package/dist/interfaces/parsed-subpath.d.mts +5 -1
  7. package/dist/internal/regex-invalid-segment.d.mts +14 -0
  8. package/dist/internal/regex-invalid-segment.mjs +11 -0
  9. package/dist/internal/regex-invalid-segment.mjs.map +6 -0
  10. package/dist/internal/resolver.mjs +6 -9
  11. package/dist/internal/resolver.mjs.map +1 -1
  12. package/dist/internal/validate-array-set.d.mts +22 -0
  13. package/dist/internal/validate-array-set.mjs +11 -0
  14. package/dist/internal/validate-array-set.mjs.map +6 -0
  15. package/dist/internal/validate-map.d.mts +23 -0
  16. package/dist/internal/validate-map.mjs +11 -0
  17. package/dist/internal/validate-map.mjs.map +6 -0
  18. package/dist/internal/validate-set.d.mts +4 -5
  19. package/dist/internal/validate-set.mjs.map +1 -1
  20. package/dist/utils/fill-modules.d.mts +1 -0
  21. package/dist/utils/fill-modules.mjs +7 -1
  22. package/dist/utils/fill-modules.mjs.map +1 -1
  23. package/dist/utils/find-requires.mjs +1 -1
  24. package/dist/utils/find-subpath.d.mts +2 -1
  25. package/dist/utils/find-subpath.mjs +2 -0
  26. package/dist/utils/find-subpath.mjs.map +1 -1
  27. package/dist/utils/get-format.d.mts +8 -1
  28. package/dist/utils/get-format.mjs +22 -11
  29. package/dist/utils/get-format.mjs.map +1 -1
  30. package/dist/utils/get-source.d.mts +7 -0
  31. package/dist/utils/get-source.mjs +13 -6
  32. package/dist/utils/get-source.mjs.map +1 -1
  33. package/dist/utils/is-absolute-specifier.d.mts +1 -0
  34. package/dist/utils/is-absolute-specifier.mjs +2 -0
  35. package/dist/utils/is-absolute-specifier.mjs.map +1 -1
  36. package/dist/utils/is-bare-specifier.d.mts +1 -0
  37. package/dist/utils/is-bare-specifier.mjs +2 -0
  38. package/dist/utils/is-bare-specifier.mjs.map +1 -1
  39. package/dist/utils/is-exports-sugar.mjs +1 -1
  40. package/dist/utils/is-exports-sugar.mjs.map +1 -1
  41. package/dist/utils/is-relative-specifier.mjs.map +1 -1
  42. package/dist/utils/parse-module-id.d.mts +1 -3
  43. package/dist/utils/parse-module-id.mjs +3 -0
  44. package/dist/utils/parse-module-id.mjs.map +1 -1
  45. package/dist/utils/parse-subpath.d.mts +1 -2
  46. package/dist/utils/parse-subpath.mjs +115 -5
  47. package/dist/utils/parse-subpath.mjs.map +1 -1
  48. package/dist/utils/read-package-json.mjs +7 -5
  49. package/dist/utils/read-package-json.mjs.map +1 -1
  50. package/dist/utils/resolve-alias.mjs +3 -2
  51. package/dist/utils/resolve-alias.mjs.map +1 -1
  52. package/dist/utils/resolve-module.mjs +16 -1
  53. package/dist/utils/resolve-module.mjs.map +1 -1
  54. package/dist/utils/to-bare-specifier.mjs +8 -10
  55. package/dist/utils/to-bare-specifier.mjs.map +1 -1
  56. package/dist/utils/to-relative-specifier.d.mts +2 -0
  57. package/dist/utils/to-relative-specifier.mjs +3 -0
  58. package/dist/utils/to-relative-specifier.mjs.map +1 -1
  59. package/dist/utils/to-url.d.mts +2 -3
  60. package/dist/utils/to-url.mjs.map +1 -1
  61. package/dist/utils/validate-assertions.mjs +2 -0
  62. package/dist/utils/validate-assertions.mjs.map +1 -1
  63. package/dist/utils/validate-exports.d.mts +2 -2
  64. package/dist/utils/validate-exports.mjs +2 -2
  65. package/dist/utils/validate-exports.mjs.map +1 -1
  66. package/package.json +34 -41
  67. package/src/interfaces/options-get-format.ts +2 -14
  68. package/src/interfaces/options-get-source.ts +2 -14
  69. package/src/interfaces/options-parse-subpath.ts +20 -0
  70. package/src/interfaces/parsed-subpath.ts +10 -1
  71. package/src/internal/regex-invalid-segment.ts +23 -0
  72. package/src/internal/resolver.ts +7 -22
  73. package/src/internal/validate-array-set.ts +32 -0
  74. package/src/internal/validate-map.ts +33 -0
  75. package/src/internal/validate-set.ts +4 -5
  76. package/src/utils/fill-modules.ts +10 -1
  77. package/src/utils/find-requires.ts +1 -1
  78. package/src/utils/find-subpath.ts +11 -2
  79. package/src/utils/get-format.ts +33 -12
  80. package/src/utils/get-source.ts +21 -6
  81. package/src/utils/is-absolute-specifier.ts +5 -0
  82. package/src/utils/is-bare-specifier.ts +5 -0
  83. package/src/utils/is-exports-sugar.ts +1 -1
  84. package/src/utils/is-relative-specifier.ts +1 -0
  85. package/src/utils/parse-module-id.ts +6 -3
  86. package/src/utils/parse-subpath.ts +154 -9
  87. package/src/utils/read-package-json.ts +8 -3
  88. package/src/utils/resolve-alias.ts +3 -2
  89. package/src/utils/resolve-module.ts +40 -11
  90. package/src/utils/to-bare-specifier.ts +10 -12
  91. package/src/utils/to-relative-specifier.ts +7 -0
  92. package/src/utils/to-url.ts +2 -3
  93. package/src/utils/validate-assertions.ts +2 -0
  94. package/src/utils/validate-exports.ts +5 -5
  95. package/changelog.config.ts +0 -404
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file Internal - validateMap
3
+ * @module mlly/internal/validateMap
4
+ */
5
+
6
+ import { ERR_INVALID_ARG_TYPE, type NodeError } from '@flex-development/errnode'
7
+
8
+ /**
9
+ * Checks if given `value` is a {@linkcode Map}.
10
+ *
11
+ * Throws [`ERR_INVALID_ARG_TYPE`][1] if the `value` is not a {@linkcode Map}.
12
+ *
13
+ * [1]: https://nodejs.org/api/errors.html#err_invalid_arg_value
14
+ *
15
+ * @see {@linkcode ERR_INVALID_ARG_TYPE}
16
+ *
17
+ * @template K - Map key type(s)
18
+ * @template V - Item type(s)
19
+ *
20
+ * @param {unknown} value - Value supplied by user
21
+ * @param {string} name - Name of invalid argument or property
22
+ * @return {value is Map<K, V>} `true` if `value` is a {@linkcode Map}
23
+ * @throws {NodeError<TypeError>} If `value` is not a {@linkcode Map}
24
+ */
25
+ function validateMap<K extends string = string, V = unknown>(
26
+ value: unknown,
27
+ name: string
28
+ ): value is Map<K, V> {
29
+ if (value instanceof Map) return true
30
+ throw new ERR_INVALID_ARG_TYPE(name, ['Map'], value)
31
+ }
32
+
33
+ export default validateMap
@@ -6,10 +6,9 @@
6
6
  import { ERR_INVALID_ARG_TYPE, type NodeError } from '@flex-development/errnode'
7
7
 
8
8
  /**
9
- * Checks if given `value` is an instance of {@linkcode Set}.
9
+ * Checks if given `value` is a {@linkcode Set}.
10
10
  *
11
- * Throws [`ERR_INVALID_ARG_TYPE`][1] if the `value` is not a {@linkcode Set}
12
- * instance.
11
+ * Throws [`ERR_INVALID_ARG_TYPE`][1] if the `value` is not a {@linkcode Set}.
13
12
  *
14
13
  * [1]: https://nodejs.org/api/errors.html#err_invalid_arg_value
15
14
  *
@@ -19,8 +18,8 @@ import { ERR_INVALID_ARG_TYPE, type NodeError } from '@flex-development/errnode'
19
18
  *
20
19
  * @param {unknown} value - Value supplied by user
21
20
  * @param {string} name - Name of invalid argument or property
22
- * @return {value is Set} `true` if `value` is instance of {@linkcode Set}
23
- * @throws {NodeError<TypeError>} If `value` is an instance of {@linkcode Set}
21
+ * @return {value is Set<T>} `true` if `value` is a {@linkcode Set}
22
+ * @throws {NodeError<TypeError>} If `value` is not a {@linkcode Set}
24
23
  */
25
24
  function validateSet<T = unknown>(
26
25
  value: unknown,
@@ -7,7 +7,12 @@ import { SpecifierSyntaxKind } from '#src/enums'
7
7
  import type { FillModuleOptions } from '#src/interfaces'
8
8
  import regexp from '#src/internal/escape-reg-exp'
9
9
  import isFunction from '#src/internal/is-function'
10
- import { ERR_UNKNOWN_FILE_EXTENSION } from '@flex-development/errnode'
10
+ import validateArraySet from '#src/internal/validate-array-set'
11
+ import validateURLString from '#src/internal/validate-url-string'
12
+ import {
13
+ ERR_UNKNOWN_FILE_EXTENSION,
14
+ type NodeError
15
+ } from '@flex-development/errnode'
11
16
  import pathe from '@flex-development/pathe'
12
17
  import type { URL } from 'node:url'
13
18
  import CONDITIONS from './conditions'
@@ -33,6 +38,7 @@ import toRelativeSpecifier from './to-relative-specifier'
33
38
  * @param {string} code - Code to evaluate
34
39
  * @param {FillModuleOptions} options - Module fill options
35
40
  * @return {Promise<string>} `code` with fully specified module specifiers
41
+ * @throws {NodeError<TypeError>}
36
42
  */
37
43
  const fillModules = async (
38
44
  code: string,
@@ -40,6 +46,9 @@ const fillModules = async (
40
46
  ): Promise<string> => {
41
47
  const { conditions = CONDITIONS, ext, parent = import.meta.url } = options
42
48
 
49
+ validateArraySet(conditions, 'options.conditions')
50
+ validateURLString(parent, 'options.parent')
51
+
43
52
  // ensure specifiers have file extensions
44
53
  for (const statement of extractStatements(code)) {
45
54
  // do nothing if statement does not have specifier
@@ -33,7 +33,7 @@ const findRequires = (code: string = ''): RequireStatement[] => {
33
33
  * @const {RegExp} REQUIRE_REGEX
34
34
  */
35
35
  const REQUIRE_REGEX: RegExp =
36
- /(?<=^|[\s;])\b(?:(?:const\s*|let\s*|var\s*)?(?<imports>(?:[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*)|(?:[\w\t\n\r "$'*,./:{}-]+?))?\s*=?\s*(?<kind>require)\((?<specifier>["']?[\S\t\n\r]+?["']?)\)(?=;?\n?))(?<!(?:\/\/|\*).*)/gu
36
+ /(?<=^|[\s,:;([])\b(?:(?:const\s*|let\s*|var\s*)?(?:(?<=(?:const\s*|let\s*|var\s*))(?<imports>(?:[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*)|(?:[\w\t\n\r "$'*,./:{}-]+?)))?\s*=?\s*(?<kind>require)\((?<specifier>["']?[\S\t\n\r]+?["']?)\))(?<!(?:\/\/|\*).*)/gu
37
37
 
38
38
  return [...code.matchAll(REQUIRE_REGEX)].map(match => {
39
39
  const { 0: code = '', index: start = 0, groups = {} } = match
@@ -24,7 +24,8 @@ import toURL from './to-url'
24
24
  * Finds the subpath defined in `context`, a `package.json` [`exports`][1] or
25
25
  * [`imports`][2] field, that maps to the given package `target`.
26
26
  *
27
- * Supports extensionless targets. Returns `null` if a subpath is not found.
27
+ * Supports extensionless targets and targets without explicit `'/index'` usage.
28
+ * Returns `null` if a subpath is not found.
28
29
  *
29
30
  * [1]: https://nodejs.org/api/packages.html#exports
30
31
  * [2]: https://nodejs.org/api/packages.html#imports
@@ -181,10 +182,17 @@ const findSubpath = (
181
182
  /**
182
183
  * {@linkcode tar} without file extension.
183
184
  *
184
- * @const {string} tar_no_ext
185
+ * @const {string} tar_ne
185
186
  */
186
187
  const tar_ne: string = pathe.changeExt(tar, '')
187
188
 
189
+ /**
190
+ * {@linkcode tar_ne} without `'/index'`.
191
+ *
192
+ * @const {string} tar_ni
193
+ */
194
+ const tar_ni: string = tar_ne.replace(/\/index$/, '')
195
+
188
196
  /**
189
197
  * Index of {@linkcode PATTERN_CHARACTER} in {@linkcode tar}.
190
198
  *
@@ -196,6 +204,7 @@ const findSubpath = (
196
204
  // target is an exactish match
197
205
  case target === tar:
198
206
  case target === tar_ne:
207
+ case target === tar_ni && tar_ne.endsWith('/index'):
199
208
  case pattern === -1 && (target === tar || target === tar_ne):
200
209
  subpath = pkgsubpath
201
210
  break
@@ -5,6 +5,9 @@
5
5
 
6
6
  import { Format } from '#src/enums'
7
7
  import type { GetFormatOptions, PackageScope } from '#src/interfaces'
8
+ import validateBoolean from '#src/internal/validate-boolean'
9
+ import validateMap from '#src/internal/validate-map'
10
+ import validateObject from '#src/internal/validate-object'
8
11
  import type { ModuleId } from '#src/types'
9
12
  import {
10
13
  ERR_UNKNOWN_FILE_EXTENSION,
@@ -12,7 +15,12 @@ import {
12
15
  } from '@flex-development/errnode'
13
16
  import { isBuiltin } from '@flex-development/is-builtin'
14
17
  import pathe, { type Ext } from '@flex-development/pathe'
15
- import type { EmptyString, Nilable, Nullable } from '@flex-development/tutils'
18
+ import {
19
+ isUndefined,
20
+ type EmptyString,
21
+ type Nilable,
22
+ type Nullable
23
+ } from '@flex-development/tutils'
16
24
  import type { URL } from 'node:url'
17
25
  import EXTENSION_FORMAT_MAP from './extension-format-map'
18
26
  import lookupPackageScope from './lookup-package-scope'
@@ -22,6 +30,13 @@ import toURL from './to-url'
22
30
  /**
23
31
  * Retrieves a module format for the given module `id`.
24
32
  *
33
+ * ::: tip
34
+ * The given module `id` should be absolute (i.e. a [`file:` URL][1] or absolute
35
+ * specifier).
36
+ * :::
37
+ *
38
+ * [1]: https://nodejs.org/api/esm.html#file-urls
39
+ *
25
40
  * @see {@linkcode Format}
26
41
  * @see {@linkcode GetFormatOptions}
27
42
  * @see {@linkcode ModuleId}
@@ -38,21 +53,27 @@ const getFormat = async (
38
53
  options: GetFormatOptions = {}
39
54
  ): Promise<Nilable<Format>> => {
40
55
  const {
41
- base,
42
- experimental_json_modules = true,
43
- experimental_network_imports = false,
44
- experimental_wasm_modules = false,
56
+ experimental_json_modules: json_modules = true,
57
+ experimental_network_imports: network_imports = false,
58
+ experimental_wasm_modules: wasm_modules = false,
45
59
  extension_format_map = EXTENSION_FORMAT_MAP,
46
60
  ignore_errors = false,
47
- req
61
+ req = {}
48
62
  } = options
49
63
 
64
+ validateBoolean(json_modules, 'options.experimental_json_modules')
65
+ validateBoolean(network_imports, 'options.experimental_network_imports')
66
+ validateBoolean(wasm_modules, 'options.experimental_wasm_modules')
67
+ validateBoolean(ignore_errors, 'options.ignore_errors')
68
+ validateMap(extension_format_map, 'options.extension_format_map')
69
+ !isUndefined(req) && validateObject(req, 'options.req')
70
+
50
71
  /**
51
72
  * Module {@linkcode id} as {@linkcode URL}.
52
73
  *
53
74
  * @const {URL} url
54
75
  */
55
- const url: URL = toURL(id, base)
76
+ const url: URL = toURL(id)
56
77
 
57
78
  /**
58
79
  * Extracts a [MIME type][1] from a {@linkcode URL} href, {@linkcode URL}
@@ -94,10 +115,10 @@ const getFormat = async (
94
115
  format = Format.MODULE
95
116
  break
96
117
  case 'application/json':
97
- format = experimental_json_modules ? Format.JSON : null
118
+ format = json_modules ? Format.JSON : null
98
119
  break
99
120
  case 'application/wasm':
100
- format = experimental_wasm_modules ? Format.WASM : null
121
+ format = wasm_modules ? Format.WASM : null
101
122
  break
102
123
  default:
103
124
  break
@@ -149,8 +170,8 @@ const getFormat = async (
149
170
  format = extension_format_map.get(ext)!
150
171
 
151
172
  switch (true) {
152
- case format === Format.JSON && !experimental_json_modules:
153
- case format === Format.WASM && !experimental_wasm_modules:
173
+ case format === Format.JSON && !json_modules:
174
+ case format === Format.WASM && !wasm_modules:
154
175
  format = ignore_errors ? undefined : null
155
176
  break
156
177
  default:
@@ -200,7 +221,7 @@ const getFormat = async (
200
221
  break
201
222
  case 'http:':
202
223
  case 'https:':
203
- if (experimental_network_imports) {
224
+ if (network_imports) {
204
225
  const { default: fetch } = await import('node-fetch')
205
226
  const { headers } = await fetch(url.href, req)
206
227
  format = mimeToFormat(headers.get('content-type'), true)
@@ -5,11 +5,15 @@
5
5
 
6
6
  import { Format } from '#src/enums'
7
7
  import type { GetSourceOptions } from '#src/interfaces'
8
+ import validateBoolean from '#src/internal/validate-boolean'
9
+ import validateObject from '#src/internal/validate-object'
10
+ import validateString from '#src/internal/validate-string'
8
11
  import type { ModuleId } from '#src/types'
9
12
  import {
10
13
  ERR_UNSUPPORTED_ESM_URL_SCHEME,
11
14
  type NodeError
12
15
  } from '@flex-development/errnode'
16
+ import { isUndefined } from '@flex-development/tutils'
13
17
  import fs from 'node:fs/promises'
14
18
  import os from 'node:os'
15
19
  import type { URL } from 'node:url'
@@ -20,6 +24,13 @@ import toURL from './to-url'
20
24
  /**
21
25
  * Retrieves source code for the given module `id`.
22
26
  *
27
+ * ::: tip
28
+ * The given module `id` should be absolute (i.e. a [`file:` URL][1] or absolute
29
+ * specifier).
30
+ * :::
31
+ *
32
+ * [1]: https://nodejs.org/api/esm.html#file-urls
33
+ *
23
34
  * @see {@linkcode GetSourceOptions}
24
35
  * @see {@linkcode ModuleId}
25
36
  * @see https://nodejs.org/docs/latest-v19.x/api/esm.html#loadurl-context-nextload
@@ -36,13 +47,17 @@ const getSource = async (
36
47
  options: GetSourceOptions = {}
37
48
  ): Promise<Uint8Array | string | undefined> => {
38
49
  const {
39
- base,
40
- experimental_network_imports = false,
50
+ experimental_network_imports: network_imports = false,
41
51
  format,
42
52
  ignore_errors = false,
43
- req
53
+ req = {}
44
54
  } = options
45
55
 
56
+ validateBoolean(network_imports, 'options.experimental_network_imports')
57
+ !isUndefined(format) && validateString(format, 'options.format')
58
+ validateBoolean(ignore_errors, 'options.ignore_errors')
59
+ !isUndefined(req) && validateObject(req, 'options.req')
60
+
46
61
  // exit early if format is Format.BUILTIN
47
62
  if (format === Format.BUILTIN) return undefined
48
63
 
@@ -51,7 +66,7 @@ const getSource = async (
51
66
  *
52
67
  * @const {URL} url
53
68
  */
54
- const url: URL = toURL(id, base)
69
+ const url: URL = toURL(id)
55
70
 
56
71
  /**
57
72
  * [`ERR_UNSUPPORTED_ESM_URL_SCHEME`][1] check.
@@ -89,7 +104,7 @@ const getSource = async (
89
104
  break
90
105
  case 'http:':
91
106
  case 'https:':
92
- if (experimental_network_imports) {
107
+ if (network_imports) {
93
108
  const { default: fetch } = await import('node-fetch')
94
109
  source = await (await fetch(url.href, req)).text()
95
110
  } else {
@@ -114,7 +129,7 @@ const getSource = async (
114
129
  const schemes: string[] = ['data', 'file']
115
130
 
116
131
  // update supported schemes if support for network based modules is enabled
117
- if (experimental_network_imports) schemes.push('http', 'https')
132
+ if (network_imports) schemes.push('http', 'https')
118
133
 
119
134
  throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(
120
135
  url,
@@ -3,6 +3,8 @@
3
3
  * @module mlly/utils/isAbsoluteSpecifier
4
4
  */
5
5
 
6
+ import validateString from '#src/internal/validate-string'
7
+ import type { NodeError } from '@flex-development/errnode'
6
8
  import pathe from '@flex-development/pathe'
7
9
  import { URL } from 'node:url'
8
10
 
@@ -18,8 +20,11 @@ import { URL } from 'node:url'
18
20
  *
19
21
  * @param {string} specifier - Specifier to evaluate
20
22
  * @return {boolean} `true` if `specifier` is absolute specifier
23
+ * @throws {NodeError<TypeError>} If `specifier` is not a string
21
24
  */
22
25
  const isAbsoluteSpecifier = (specifier: string): boolean => {
26
+ validateString(specifier, 'specifier')
27
+
23
28
  /**
24
29
  * Absolute specifier check.
25
30
  *
@@ -3,6 +3,8 @@
3
3
  * @module mlly/utils/isBareSpecifier
4
4
  */
5
5
 
6
+ import validateString from '#src/internal/validate-string'
7
+ import type { NodeError } from '@flex-development/errnode'
6
8
  import isAbsoluteSpecifier from './is-absolute-specifier'
7
9
  import isRelativeSpecifier from './is-relative-specifier'
8
10
 
@@ -18,8 +20,11 @@ import isRelativeSpecifier from './is-relative-specifier'
18
20
  *
19
21
  * @param {string} specifier - Specifier to evaluate
20
22
  * @return {boolean} `true` if `specifier` is bare specifier
23
+ * @throws {NodeError<TypeError>} If `specifier` is not a string
21
24
  */
22
25
  const isBareSpecifier = (specifier: string): boolean => {
26
+ validateString(specifier, 'specifier')
27
+
23
28
  return (
24
29
  specifier.trim().length > 0 &&
25
30
  !isAbsoluteSpecifier(specifier) &&
@@ -65,7 +65,7 @@ const isExportsSugar = (
65
65
  validateExports(exports, pkg, parent)
66
66
 
67
67
  // check for exports sugar
68
- sugar = !Object.getOwnPropertyNames(exports)[0]?.startsWith('.')
68
+ sugar = !(Object.getOwnPropertyNames(exports)[0]?.startsWith('.') ?? true)
69
69
  }
70
70
 
71
71
  return sugar
@@ -23,6 +23,7 @@ import pathe from '@flex-development/pathe'
23
23
  */
24
24
  const isRelativeSpecifier = (specifier: string): boolean => {
25
25
  validateString(specifier, 'specifier')
26
+
26
27
  return specifier.startsWith('.')
27
28
  ? specifier.length === 1 || specifier[1] === pathe.sep
28
29
  ? true
@@ -7,6 +7,7 @@ import type { ParseModuleIdOptions, ParsedModuleId } from '#src/interfaces'
7
7
  import ENCODED_SEP_REGEX from '#src/internal/regex-encoded-sep'
8
8
  import INTERNAL_SPECIFIER_REGEX from '#src/internal/regex-internal-specifier'
9
9
  import PACKAGE_PATH_REGEX from '#src/internal/regex-package-path'
10
+ import validateBoolean from '#src/internal/validate-boolean'
10
11
  import validateURLString from '#src/internal/validate-url-string'
11
12
  import type { ModuleId } from '#src/types'
12
13
  import {
@@ -30,9 +31,7 @@ import toNodeURL from './to-node-url'
30
31
  * @param {ModuleId} id - Module id to parse
31
32
  * @param {ParseModuleIdOptions?} [options={}] - Parsing options
32
33
  * @return {ParsedModuleId} Object representing `id`
33
- * @throws {NodeError<TypeError>} If `id` is not an instance of {@linkcode URL}
34
- * or a string, if `id` includes encoded path separators, or if `id` is invalid
35
- * according to `options.internal` or `options.pkgname`
34
+ * @throws {NodeError<TypeError>}
36
35
  */
37
36
  const parseModuleId = (
38
37
  id: ModuleId,
@@ -46,6 +45,10 @@ const parseModuleId = (
46
45
  // ensure id is an instance of URL or a string
47
46
  validateURLString(id, 'id')
48
47
 
48
+ // ensure option schemas
49
+ validateBoolean(internal, 'options.internal')
50
+ validateBoolean(pkgname, 'options.pkgname')
51
+
49
52
  // ensure id a string without leading and trailing spaces
50
53
  id = id instanceof URL ? id.href : id.trim()
51
54
 
@@ -9,18 +9,28 @@ import type {
9
9
  ParsedSubpath
10
10
  } from '#src/interfaces'
11
11
  import getSubpaths from '#src/internal/get-subpaths'
12
+ import isArrayIndex from '#src/internal/is-array-index'
13
+ import invalidSegmentRegex from '#src/internal/regex-invalid-segment'
14
+ import PACKAGE_NAME_REGEX from '#src/internal/regex-package-name'
15
+ import validateArraySet from '#src/internal/validate-array-set'
16
+ import validateBoolean from '#src/internal/validate-boolean'
12
17
  import validateString from '#src/internal/validate-string'
13
18
  import validateURLString from '#src/internal/validate-url-string'
14
19
  import {
20
+ ERR_INVALID_PACKAGE_CONFIG,
21
+ ERR_INVALID_PACKAGE_TARGET,
15
22
  ERR_PACKAGE_IMPORT_NOT_DEFINED,
16
23
  ERR_PACKAGE_PATH_NOT_EXPORTED,
24
+ ErrorCode,
17
25
  type NodeError
18
26
  } from '@flex-development/errnode'
19
27
  import pathe from '@flex-development/pathe'
20
28
  import type { Exports, Imports } from '@flex-development/pkg-types'
21
- import { CompareResult, type Nullable } from '@flex-development/tutils'
29
+ import { CompareResult, isNIL, type Nullable } from '@flex-development/tutils'
22
30
  import { URL, fileURLToPath, pathToFileURL } from 'node:url'
23
31
  import compareSubpaths from './compare-subpaths'
32
+ import CONDITIONS from './conditions'
33
+ import isExportsSugar from './is-exports-sugar'
24
34
  import parseModuleId from './parse-module-id'
25
35
  import PATTERN_CHARACTER from './pattern-character'
26
36
 
@@ -48,21 +58,30 @@ import PATTERN_CHARACTER from './pattern-character'
48
58
  * @param {Exports | Imports | undefined} context - Package context
49
59
  * @param {ParseSubpathOptions} options - Parsing options
50
60
  * @return {ParsedSubpath} Object representing package subpath
51
- * @throws {NodeError<Error | TypeError>} If `specifier` is not a string or the
52
- * subpath defined in `specifier` is not defined in `context`
61
+ * @throws {NodeError<Error | TypeError>}
53
62
  */
54
63
  const parseSubpath = (
55
64
  specifier: string,
56
65
  context: Exports | Imports | undefined,
57
66
  options: ParseSubpathOptions
58
67
  ): ParsedSubpath => {
59
- const { dir, internal = specifier.startsWith('#'), parent } = options
68
+ const {
69
+ condition = 'default',
70
+ conditions = CONDITIONS,
71
+ dir,
72
+ internal = specifier.startsWith('#'),
73
+ parent
74
+ } = options
60
75
 
61
76
  // ensure specifier is a string
62
77
  validateString(specifier, 'specifier')
63
78
 
64
- // ensure dir is an instance of URL or a string
79
+ // ensure option schemas
80
+ validateString(condition, 'options.condition')
81
+ validateArraySet(conditions, 'options.conditions')
65
82
  validateURLString(dir, 'options.dir')
83
+ validateBoolean(internal, 'options.internal')
84
+ validateURLString(parent, 'options.parent')
66
85
 
67
86
  /**
68
87
  * Parsed module id.
@@ -156,9 +175,6 @@ const parseSubpath = (
156
175
  if (key === null) {
157
176
  let { parent } = options
158
177
 
159
- // ensure parent is an instance of URL or a string
160
- validateURLString(parent, 'options.parent')
161
-
162
178
  // ensure parent is a path
163
179
  parent = fileURLToPath(parent)
164
180
 
@@ -167,7 +183,136 @@ const parseSubpath = (
167
183
  : new ERR_PACKAGE_PATH_NOT_EXPORTED(pkgdir, id.path, parent)
168
184
  }
169
185
 
170
- return { base, internal, key, raw: id.path, specifier: id.raw }
186
+ /**
187
+ * Finds the package target string specified by {@linkcode key}.
188
+ *
189
+ * @param {Exports | undefined} data - Initial package target
190
+ * @return {Nullable<string>} Package target string or `null`
191
+ * @throws {NodeError<Error | TypeError>}
192
+ */
193
+ const findPackageTarget = (data: Exports | undefined): Nullable<string> => {
194
+ /**
195
+ * Package target.
196
+ *
197
+ * @var {Nullable<string>} target
198
+ */
199
+ let target: Nullable<string> = null
200
+
201
+ switch (true) {
202
+ case isNIL(data):
203
+ target = null
204
+ break
205
+ case Array.isArray(data):
206
+ /**
207
+ * Possible package target search error.
208
+ *
209
+ * @var {NodeError | undefined} error
210
+ */
211
+ let error: NodeError | undefined
212
+
213
+ // try finding package target based on first match in search context
214
+ for (const item of data as string[]) {
215
+ try {
216
+ target = findPackageTarget(item)
217
+ } catch (e: unknown) {
218
+ error = e as NodeError
219
+
220
+ /* c8 ignore next */
221
+ if (error.code !== ErrorCode.ERR_INVALID_PACKAGE_TARGET) throw error
222
+
223
+ continue
224
+ }
225
+
226
+ // stop search attempts if target was found
227
+ if (target) {
228
+ error = undefined
229
+ break
230
+ }
231
+ }
232
+
233
+ // throw if error was encountered
234
+ if (error) throw error
235
+
236
+ break
237
+ case typeof data === 'object':
238
+ data = data as Record<string, Exports>
239
+
240
+ // try finding package target based on condition
241
+ for (const property of Object.getOwnPropertyNames(data)) {
242
+ if (isArrayIndex(property)) {
243
+ throw new ERR_INVALID_PACKAGE_CONFIG(
244
+ fileURLToPath(pkg),
245
+ fileURLToPath(parent),
246
+ '"exports" cannot contain numeric property keys'
247
+ )
248
+ }
249
+
250
+ // check conditions
251
+ if (property === condition || new Set(conditions).has(property)) {
252
+ target = findPackageTarget(data[property])
253
+ if (target) break
254
+ }
255
+ }
256
+
257
+ break
258
+ case typeof data === 'string':
259
+ target = data as string
260
+
261
+ switch (true) {
262
+ case internal && PACKAGE_NAME_REGEX.test(target):
263
+ break
264
+ case target.startsWith('.' + pathe.sep):
265
+ // check target for invalid segments
266
+ if (invalidSegmentRegex().test(target.slice(2))) {
267
+ if (invalidSegmentRegex('deprecated').test(target.slice(2))) {
268
+ throw new ERR_INVALID_PACKAGE_TARGET(
269
+ fileURLToPath(dir),
270
+ key!,
271
+ target,
272
+ internal,
273
+ fileURLToPath(parent)
274
+ )
275
+ }
276
+ }
277
+
278
+ break
279
+ default:
280
+ throw new ERR_INVALID_PACKAGE_TARGET(
281
+ fileURLToPath(dir),
282
+ key!,
283
+ target,
284
+ internal,
285
+ fileURLToPath(parent)
286
+ )
287
+ }
288
+
289
+ break
290
+ default:
291
+ throw new ERR_INVALID_PACKAGE_TARGET(
292
+ fileURLToPath(dir),
293
+ key!,
294
+ data,
295
+ internal,
296
+ fileURLToPath(parent)
297
+ )
298
+ }
299
+
300
+ return target
301
+ }
302
+
303
+ // convert exports to object if using exports main sugar
304
+ if (!internal && isExportsSugar(context, pkg, parent)) {
305
+ context = { '.': context } as Record<string, Exports>
306
+ }
307
+
308
+ return {
309
+ base,
310
+ internal,
311
+ key,
312
+ raw: id.path,
313
+ specifier: id.raw,
314
+ target: findPackageTarget((context as Record<string, Exports>)[key])
315
+ }
171
316
  }
172
317
 
173
318
  export default parseSubpath
@@ -13,7 +13,12 @@ import {
13
13
  } from '@flex-development/errnode'
14
14
  import pathe from '@flex-development/pathe'
15
15
  import type { PackageJson } from '@flex-development/pkg-types'
16
- import { isEmptyString, isNIL, type Nullable } from '@flex-development/tutils'
16
+ import {
17
+ isEmptyString,
18
+ isNIL,
19
+ isUndefined,
20
+ type Nullable
21
+ } from '@flex-development/tutils'
17
22
  import fs from 'node:fs'
18
23
  import { fileURLToPath } from 'node:url'
19
24
  import toURL from './to-url'
@@ -44,10 +49,10 @@ const readPackageJson = (
44
49
  validateURLString(dir, 'dir')
45
50
 
46
51
  // ensure specifier is a string
47
- if (specifier !== undefined) validateString(specifier, 'specifier')
52
+ !isUndefined(specifier) && validateString(specifier, 'specifier')
48
53
 
49
54
  // ensure parent is an instance of URL or a string
50
- if (parent !== undefined) validateURLString(parent, 'parent')
55
+ !isUndefined(parent) && validateURLString(parent, 'parent')
51
56
 
52
57
  // ensure dir is a path
53
58
  dir = fileURLToPath(toURL(dir))
@@ -5,9 +5,9 @@
5
5
 
6
6
  import type { ParsedModuleId, ResolveAliasOptions } from '#src/interfaces'
7
7
  import regexp from '#src/internal/escape-reg-exp'
8
+ import validateArraySet from '#src/internal/validate-array-set'
8
9
  import validateBoolean from '#src/internal/validate-boolean'
9
10
  import validateObject from '#src/internal/validate-object'
10
- import validateSet from '#src/internal/validate-set'
11
11
  import validateString from '#src/internal/validate-string'
12
12
  import validateURLString from '#src/internal/validate-url-string'
13
13
  import type { NodeError } from '@flex-development/errnode'
@@ -60,8 +60,9 @@ const resolveAlias = async (
60
60
  validateBoolean(absolute, 'options.absolute')
61
61
  validateObject(aliases, 'options.aliases')
62
62
  validateString(condition, 'options.condition')
63
+ validateArraySet(conditions, 'options.conditions')
63
64
  validateURLString(cwd, 'options.cwd')
64
- validateSet(extensions, 'options.extensions')
65
+ validateArraySet(extensions, 'options.extensions')
65
66
  validateURLString(parent, 'options.parent')
66
67
  validateBoolean(preserveSymlinks, 'options.preserveSymlinks')
67
68