@jsenv/urls 1.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.
- package/docs/url_meta/advanced.md +28 -0
- package/docs/url_meta/demo/apply_associations.mjs +27 -0
- package/docs/url_meta/demo/full_match.mjs +7 -0
- package/docs/url_meta/demo/partial_match.mjs +7 -0
- package/docs/url_meta/demo/resolve_associations.mjs +12 -0
- package/docs/url_meta/notes.md +26 -0
- package/docs/url_meta/pattern_matching.md +71 -0
- package/docs/url_meta/readme.md +87 -0
- package/main.js +27 -0
- package/package.json +28 -0
- package/src/caller_position.js +20 -0
- package/src/data_url.js +50 -0
- package/src/inline_content_url_generator.js +21 -0
- package/src/meta/aliases.js +28 -0
- package/src/meta/as_flat_associations.js +28 -0
- package/src/meta/assertions.js +57 -0
- package/src/meta/associations.js +25 -0
- package/src/meta/pattern_matching.js +245 -0
- package/src/meta/resolve_associations.js +25 -0
- package/src/meta/url_child_may_match.js +47 -0
- package/src/meta/url_meta.js +13 -0
- package/src/url_trace.js +229 -0
- package/src/url_utils.js +107 -0
- package/src/windows_file_path_utils.js +16 -0
- package/test/data_url.test.mjs +57 -0
- package/test/meta/aliasing.test.mjs +47 -0
- package/test/meta/as_flat_associations/basic.test.mjs +64 -0
- package/test/meta/assertions.test.mjs +23 -0
- package/test/meta/get_url_meta/any.test.mjs +94 -0
- package/test/meta/get_url_meta/basic.test.mjs +226 -0
- package/test/meta/get_url_meta/complex.test.mjs +203 -0
- package/test/meta/get_url_meta/double_wildcard_after.test.mjs +68 -0
- package/test/meta/get_url_meta/double_wildcard_around.test.mjs +42 -0
- package/test/meta/get_url_meta/double_wildcard_before.test.mjs +55 -0
- package/test/meta/get_url_meta/inheritance.test.mjs +59 -0
- package/test/meta/is_plain_object.test.mjs +27 -0
- package/test/meta/pattern_matching/basic.test.mjs +100 -0
- package/test/meta/pattern_matching/common.test.mjs +145 -0
- package/test/meta/pattern_matching/complex.test.mjs +159 -0
- package/test/meta/pattern_matching/contains.test.mjs +46 -0
- package/test/meta/pattern_matching/doublewildcard_after.test.mjs +81 -0
- package/test/meta/pattern_matching/doublewildcard_around.test.mjs +42 -0
- package/test/meta/pattern_matching/doublewildcard_before.test.mjs +68 -0
- package/test/meta/pattern_matching/doublewildcard_inside.test.mjs +42 -0
- package/test/meta/pattern_matching/trailing_slash.test.mjs +55 -0
- package/test/meta/pattern_matching/wildcard_after.test.mjs +68 -0
- package/test/meta/pattern_matching/wildcard_around.test.mjs +94 -0
- package/test/meta/pattern_matching/wildcard_before.test.mjs +55 -0
- package/test/meta/pattern_matching/wildcard_inside.test.mjs +68 -0
- package/test/meta/resolve_associations/basic.test.mjs +42 -0
- package/test/meta/resolve_associations/http_pattern.test.mjs +35 -0
- package/test/meta/url_child_may_match/inheritance.test.mjs +71 -0
- package/test/meta/url_child_may_match/url_child_may_match.test.mjs +166 -0
- package/test/url_trace.test.mjs +224 -0
- package/test.mjs +13 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# urlChildMayMatch
|
|
2
|
+
|
|
3
|
+
_urlChildMayMatch_ is a function designed to ignore directory content that would never have specific metas.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { URL_META } from "@jsenv/urls"
|
|
7
|
+
|
|
8
|
+
const associations = {
|
|
9
|
+
color: {
|
|
10
|
+
"file:///**/*": "blue",
|
|
11
|
+
"file:///**/node_modules/": "green",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const firstUrlCanHaveFilesWithColorBlue = URL_META.urlChildMayMatch({
|
|
16
|
+
url: "file:///src/",
|
|
17
|
+
associations,
|
|
18
|
+
predicate: ({ color }) => color === "blue",
|
|
19
|
+
})
|
|
20
|
+
firstUrlCanHaveFilesWithColorBlue // true
|
|
21
|
+
|
|
22
|
+
const secondUrlCanHaveFileWithColorBlue = URL_META.urlChildMayMatch({
|
|
23
|
+
url: "file:///node_modules/src/",
|
|
24
|
+
associations,
|
|
25
|
+
predicate: ({ color }) => color === "blue",
|
|
26
|
+
})
|
|
27
|
+
secondUrlCanHaveFileWithColorBlue // false
|
|
28
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { URL_META } from "@jsenv/urls"
|
|
2
|
+
|
|
3
|
+
const associations = {
|
|
4
|
+
insideSrc: {
|
|
5
|
+
"file:///src/": true,
|
|
6
|
+
},
|
|
7
|
+
extensionIsJs: {
|
|
8
|
+
"file:///**/*.js": true,
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const urlA = "file:///src/file.js"
|
|
13
|
+
const urlB = "file:///src/file.json"
|
|
14
|
+
console.log(
|
|
15
|
+
`${urlA}: ${JSON.stringify(
|
|
16
|
+
URL_META.applyAssociations({ url: urlA, associations }),
|
|
17
|
+
null,
|
|
18
|
+
" ",
|
|
19
|
+
)}`,
|
|
20
|
+
)
|
|
21
|
+
console.log(
|
|
22
|
+
`${urlB}: ${JSON.stringify(
|
|
23
|
+
URL_META.applyAssociations({ url: urlB, associations }),
|
|
24
|
+
null,
|
|
25
|
+
" ",
|
|
26
|
+
)}`,
|
|
27
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
```js
|
|
2
|
+
// Name of variables in the code below corresponds
|
|
3
|
+
// to the terminology used in the codebase and documentation
|
|
4
|
+
const pattern = "**/*"
|
|
5
|
+
const value = { visible: true }
|
|
6
|
+
const valueMap = {
|
|
7
|
+
[pattern]: value,
|
|
8
|
+
}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## associations
|
|
12
|
+
|
|
13
|
+
_associations composition_
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const pattern = "**/*/"
|
|
17
|
+
const key = "visible"
|
|
18
|
+
const value = true
|
|
19
|
+
const meta = { [key]: value }
|
|
20
|
+
const valueMap = {
|
|
21
|
+
[pattern]: meta,
|
|
22
|
+
}
|
|
23
|
+
const associations = {
|
|
24
|
+
[key]: valueMap,
|
|
25
|
+
}
|
|
26
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# applyPatternMatching
|
|
2
|
+
|
|
3
|
+
_applyPatternMatching_ is a function returning a _matchResult_ indicating if and how a _pattern_ matches an _url_.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { URL_META } from "@jsenv/urls"
|
|
7
|
+
|
|
8
|
+
const matchResult = URL_META.applyPatternMatching({
|
|
9
|
+
pattern: "file:///**/*",
|
|
10
|
+
url: "file://Users/directory/file.js",
|
|
11
|
+
})
|
|
12
|
+
matchResult.matched // true
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## pattern
|
|
16
|
+
|
|
17
|
+
_pattern_ parameter is a string looking like an url but where `*` and `**` can be used so that one specifier can match several url.
|
|
18
|
+
|
|
19
|
+
This parameter is **required**.
|
|
20
|
+
|
|
21
|
+
## url
|
|
22
|
+
|
|
23
|
+
_url_ parameter is a string representing a url.
|
|
24
|
+
|
|
25
|
+
This parameter is **required**.
|
|
26
|
+
|
|
27
|
+
## matchResult
|
|
28
|
+
|
|
29
|
+
_matchResult_ represents if and how a _pattern_ matches an _url_.
|
|
30
|
+
|
|
31
|
+
### Matching example
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { URL_META } from "@jsenv/urls"
|
|
35
|
+
|
|
36
|
+
const fullMatch = URL_META.applyPatternMatching({
|
|
37
|
+
pattern: "file:///**/*",
|
|
38
|
+
url: "file:///Users/directory/file.js",
|
|
39
|
+
})
|
|
40
|
+
console.log(JSON.stringify(fullMatch, null, " "))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"matched": true,
|
|
46
|
+
"patternIndex": 12,
|
|
47
|
+
"urlIndex": 31,
|
|
48
|
+
"matchGroups": ["file.js"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Partial matching example
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { applyPatternMatching } from "@jsenv/urlss"
|
|
56
|
+
|
|
57
|
+
const partialMatch = URL_META.applyPatternMatching({
|
|
58
|
+
pattern: "file:///*.js",
|
|
59
|
+
url: "file:///file.jsx",
|
|
60
|
+
})
|
|
61
|
+
console.log(JSON.stringify(partialMatch, null, " "))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"matched": false,
|
|
67
|
+
"patternIndex": 12,
|
|
68
|
+
"urlIndex": 15,
|
|
69
|
+
"matchGroups": ["file"]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# url meta
|
|
2
|
+
|
|
3
|
+
Associate value to urls using pattern matching.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { URL_META } from "@jsenv/urls"
|
|
7
|
+
|
|
8
|
+
// conditionally associates url and values
|
|
9
|
+
const associations = {
|
|
10
|
+
color: {
|
|
11
|
+
"file:///*": "black",
|
|
12
|
+
"file:///*.js": "red",
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
const getUrlColor = (url) => {
|
|
16
|
+
return URL_META.applyAssociations({ url, associations }).color
|
|
17
|
+
}
|
|
18
|
+
console.log(`file.json color is ${getUrlColor("file:///file.json")}`)
|
|
19
|
+
console.log(`file.js color is ${getUrlColor("file:///file.js")}`)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
_Code above logs_
|
|
23
|
+
|
|
24
|
+
```console
|
|
25
|
+
file.json color is black
|
|
26
|
+
file.js color is red
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
# Common pattern example
|
|
30
|
+
|
|
31
|
+
| pattern | Description |
|
|
32
|
+
| ------------------ | ------------------------------------ |
|
|
33
|
+
| `**/` | Everything |
|
|
34
|
+
| `*/**/` | Inside a directory |
|
|
35
|
+
| `**/.*/` | Inside directory starting with a dot |
|
|
36
|
+
| `**/node_modules/` | Inside any `node_modules` directory |
|
|
37
|
+
| `node_modules/` | Inside root `node_modules` directory |
|
|
38
|
+
| `**/*.map` | Ending with `.map` |
|
|
39
|
+
| `**/*.test.*` | Contains `.test.` |
|
|
40
|
+
| `*` | Inside the root directory only |
|
|
41
|
+
| `*/*` | Inside a directory of depth 1 |
|
|
42
|
+
|
|
43
|
+
Read more at [./pattern_matching.md](./pattern_matching.md)
|
|
44
|
+
|
|
45
|
+
# associations
|
|
46
|
+
|
|
47
|
+
_associations_ below translates into: "files are visible except thoose in .git/ directory"
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const associations = {
|
|
51
|
+
visible: {
|
|
52
|
+
"**/*/": true,
|
|
53
|
+
"**/.git/": false,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
_associations_ allows to group patterns per property which are easy to read and compose.
|
|
59
|
+
All keys in _associations_ must be absolute urls, this can be done with [resolveAssociations](#resolveAssociations).
|
|
60
|
+
|
|
61
|
+
# resolveAssociations
|
|
62
|
+
|
|
63
|
+
_resolveAssociations_ is a function resolving _associations_ keys that may contain relative urls against an _url_.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { URL_META } from "@jsenv/urls"
|
|
67
|
+
|
|
68
|
+
const associations = URL_META.resolveAssociations(
|
|
69
|
+
{
|
|
70
|
+
visible: {
|
|
71
|
+
"**/*/": true,
|
|
72
|
+
"**/.git/": false,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"file:///Users/directory/",
|
|
76
|
+
)
|
|
77
|
+
console.log(JSON.stringify(associations, null, " "))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"visible": {
|
|
83
|
+
"file:///Users/directory/**/*/": true,
|
|
84
|
+
"file:///Users/directory/**/.git/": false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
package/main.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { DataUrl } from "./src/data_url.js"
|
|
2
|
+
export { generateInlineContentUrl } from "./src/inline_content_url_generator.js"
|
|
3
|
+
export {
|
|
4
|
+
stringifyUrlTrace,
|
|
5
|
+
stringifyUrlSite,
|
|
6
|
+
humanizeUrl,
|
|
7
|
+
showSourceLocation,
|
|
8
|
+
} from "./src/url_trace.js"
|
|
9
|
+
export {
|
|
10
|
+
asUrlWithoutSearch,
|
|
11
|
+
isValidUrl,
|
|
12
|
+
normalizeUrl,
|
|
13
|
+
injectQueryParamsIntoSpecifier,
|
|
14
|
+
injectQueryParams,
|
|
15
|
+
setUrlExtension,
|
|
16
|
+
setUrlFilename,
|
|
17
|
+
ensurePathnameTrailingSlash,
|
|
18
|
+
asUrlUntilPathname,
|
|
19
|
+
} from "./src/url_utils.js"
|
|
20
|
+
export {
|
|
21
|
+
startsWithWindowsDriveLetter,
|
|
22
|
+
windowsFilePathToUrl,
|
|
23
|
+
replaceBackSlashesWithSlashes,
|
|
24
|
+
} from "./src/windows_file_path_utils.js"
|
|
25
|
+
export { getCallerPosition } from "./src/caller_position.js"
|
|
26
|
+
|
|
27
|
+
export { URL_META } from "./src/meta/url_meta.js"
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsenv/urls",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/jsenv/jsenv-core",
|
|
8
|
+
"directory": "packages/urls"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./main.js"
|
|
18
|
+
},
|
|
19
|
+
"./*": "./*"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --conditions=development ./test.mjs"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@jsenv/filesystem": "3.1.0",
|
|
26
|
+
"@jsenv/log": "1.6.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isFileSystemPath, fileSystemPathToUrl } from "@jsenv/filesystem"
|
|
2
|
+
|
|
3
|
+
export const getCallerPosition = () => {
|
|
4
|
+
const { prepareStackTrace } = Error
|
|
5
|
+
Error.prepareStackTrace = (error, stack) => {
|
|
6
|
+
Error.prepareStackTrace = prepareStackTrace
|
|
7
|
+
return stack
|
|
8
|
+
}
|
|
9
|
+
const { stack } = new Error()
|
|
10
|
+
const callerCallsite = stack[2]
|
|
11
|
+
const fileName = callerCallsite.getFileName()
|
|
12
|
+
return {
|
|
13
|
+
url:
|
|
14
|
+
fileName && isFileSystemPath(fileName)
|
|
15
|
+
? fileSystemPathToUrl(fileName)
|
|
16
|
+
: fileName,
|
|
17
|
+
line: callerCallsite.getLineNumber(),
|
|
18
|
+
column: callerCallsite.getColumnNumber(),
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/data_url.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* data:[<mediatype>][;base64],<data>
|
|
3
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs#syntax
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint-env browser, node */
|
|
7
|
+
|
|
8
|
+
export const DataUrl = {
|
|
9
|
+
parse: (string) => {
|
|
10
|
+
const afterDataProtocol = string.slice("data:".length)
|
|
11
|
+
const commaIndex = afterDataProtocol.indexOf(",")
|
|
12
|
+
const beforeComma = afterDataProtocol.slice(0, commaIndex)
|
|
13
|
+
|
|
14
|
+
let contentType
|
|
15
|
+
let base64Flag
|
|
16
|
+
if (beforeComma.endsWith(`;base64`)) {
|
|
17
|
+
contentType = beforeComma.slice(0, -`;base64`.length)
|
|
18
|
+
base64Flag = true
|
|
19
|
+
} else {
|
|
20
|
+
contentType = beforeComma
|
|
21
|
+
base64Flag = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
contentType =
|
|
25
|
+
contentType === "" ? "text/plain;charset=US-ASCII" : contentType
|
|
26
|
+
const afterComma = afterDataProtocol.slice(commaIndex + 1)
|
|
27
|
+
return {
|
|
28
|
+
contentType,
|
|
29
|
+
base64Flag,
|
|
30
|
+
data: afterComma,
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
stringify: ({ contentType, base64Flag = true, data }) => {
|
|
35
|
+
if (!contentType || contentType === "text/plain;charset=US-ASCII") {
|
|
36
|
+
// can be a buffer or a string, hence check on data.length instead of !data or data === ''
|
|
37
|
+
if (data.length === 0) {
|
|
38
|
+
return `data:,`
|
|
39
|
+
}
|
|
40
|
+
if (base64Flag) {
|
|
41
|
+
return `data:;base64,${data}`
|
|
42
|
+
}
|
|
43
|
+
return `data:,${data}`
|
|
44
|
+
}
|
|
45
|
+
if (base64Flag) {
|
|
46
|
+
return `data:${contentType};base64,${data}`
|
|
47
|
+
}
|
|
48
|
+
return `data:${contentType},${data}`
|
|
49
|
+
},
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { urlToFilename } from "@jsenv/filesystem"
|
|
2
|
+
|
|
3
|
+
export const generateInlineContentUrl = ({
|
|
4
|
+
url,
|
|
5
|
+
extension,
|
|
6
|
+
line,
|
|
7
|
+
column,
|
|
8
|
+
lineEnd,
|
|
9
|
+
columnEnd,
|
|
10
|
+
}) => {
|
|
11
|
+
const generatedName =
|
|
12
|
+
line === lineEnd
|
|
13
|
+
? `L${line}C${column}-L${lineEnd}C${columnEnd}`
|
|
14
|
+
: `L${line}-L${lineEnd}`
|
|
15
|
+
const filenameRaw = urlToFilename(url)
|
|
16
|
+
const filename = `${filenameRaw}@${generatedName}${extension}`
|
|
17
|
+
// ideally we should keep query params from url
|
|
18
|
+
// maybe we could use a custom scheme like "inline:"
|
|
19
|
+
const inlineContentUrl = new URL(filename, url).href
|
|
20
|
+
return inlineContentUrl
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { applyPatternMatching } from "./pattern_matching.js"
|
|
2
|
+
|
|
3
|
+
export const applyAliases = ({ url, aliases }) => {
|
|
4
|
+
let aliasFullMatchResult
|
|
5
|
+
const aliasMatchingKey = Object.keys(aliases).find((key) => {
|
|
6
|
+
const aliasMatchResult = applyPatternMatching({
|
|
7
|
+
pattern: key,
|
|
8
|
+
url,
|
|
9
|
+
})
|
|
10
|
+
if (aliasMatchResult.matched) {
|
|
11
|
+
aliasFullMatchResult = aliasMatchResult
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
})
|
|
16
|
+
if (!aliasMatchingKey) {
|
|
17
|
+
return url
|
|
18
|
+
}
|
|
19
|
+
const { matchGroups } = aliasFullMatchResult
|
|
20
|
+
const alias = aliases[aliasMatchingKey]
|
|
21
|
+
const parts = alias.split("*")
|
|
22
|
+
const newUrl = parts.reduce((previous, value, index) => {
|
|
23
|
+
return `${previous}${value}${
|
|
24
|
+
index === parts.length - 1 ? "" : matchGroups[index]
|
|
25
|
+
}`
|
|
26
|
+
}, "")
|
|
27
|
+
return newUrl
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isPlainObject } from "./assertions.js"
|
|
2
|
+
|
|
3
|
+
export const asFlatAssociations = (associations) => {
|
|
4
|
+
if (!isPlainObject(associations)) {
|
|
5
|
+
throw new TypeError(
|
|
6
|
+
`associations must be a plain object, got ${associations}`,
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
const flatAssociations = {}
|
|
10
|
+
Object.keys(associations).forEach((key) => {
|
|
11
|
+
const valueMap = associations[key]
|
|
12
|
+
if (!isPlainObject(valueMap)) {
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
`all associations value must be objects, found "${key}": ${valueMap}`,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
Object.keys(valueMap).forEach((pattern) => {
|
|
18
|
+
const value = valueMap[pattern]
|
|
19
|
+
const previousValue = flatAssociations[pattern]
|
|
20
|
+
flatAssociations[pattern] = previousValue
|
|
21
|
+
? { ...previousValue, [key]: value }
|
|
22
|
+
: {
|
|
23
|
+
[key]: value,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
return flatAssociations
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const assertSpecifierMetaMap = (value, checkComposition = true) => {
|
|
2
|
+
if (!isPlainObject(value)) {
|
|
3
|
+
throw new TypeError(`specifierMetaMap must be a plain object, got ${value}`)
|
|
4
|
+
}
|
|
5
|
+
if (checkComposition) {
|
|
6
|
+
const plainObject = value
|
|
7
|
+
Object.keys(plainObject).forEach((key) => {
|
|
8
|
+
assertUrlLike(key, "specifierMetaMap key")
|
|
9
|
+
const value = plainObject[key]
|
|
10
|
+
if (value !== null && !isPlainObject(value)) {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
`specifierMetaMap value must be a plain object or null, got ${value} under key ${key}`,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const assertUrlLike = (value, name = "url") => {
|
|
20
|
+
if (typeof value !== "string") {
|
|
21
|
+
throw new TypeError(`${name} must be a url string, got ${value}`)
|
|
22
|
+
}
|
|
23
|
+
if (isWindowsPathnameSpecifier(value)) {
|
|
24
|
+
throw new TypeError(
|
|
25
|
+
`${name} must be a url but looks like a windows pathname, got ${value}`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (!hasScheme(value)) {
|
|
29
|
+
throw new TypeError(
|
|
30
|
+
`${name} must be a url and no scheme found, got ${value}`,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const isPlainObject = (value) => {
|
|
36
|
+
if (value === null) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "object") {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isWindowsPathnameSpecifier = (specifier) => {
|
|
49
|
+
const firstChar = specifier[0]
|
|
50
|
+
if (!/[a-zA-Z]/.test(firstChar)) return false
|
|
51
|
+
const secondChar = specifier[1]
|
|
52
|
+
if (secondChar !== ":") return false
|
|
53
|
+
const thirdChar = specifier[2]
|
|
54
|
+
return thirdChar === "/" || thirdChar === "\\"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasScheme = (specifier) => /^[a-zA-Z]+:/.test(specifier)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { assertUrlLike, isPlainObject } from "./assertions.js"
|
|
2
|
+
import { asFlatAssociations } from "./as_flat_associations.js"
|
|
3
|
+
import { applyPatternMatching } from "./pattern_matching.js"
|
|
4
|
+
|
|
5
|
+
export const applyAssociations = ({ url, associations }) => {
|
|
6
|
+
assertUrlLike(url)
|
|
7
|
+
const flatAssociations = asFlatAssociations(associations)
|
|
8
|
+
return Object.keys(flatAssociations).reduce((previousValue, pattern) => {
|
|
9
|
+
const { matched } = applyPatternMatching({
|
|
10
|
+
pattern,
|
|
11
|
+
url,
|
|
12
|
+
})
|
|
13
|
+
if (matched) {
|
|
14
|
+
const value = flatAssociations[pattern]
|
|
15
|
+
if (isPlainObject(previousValue) && isPlainObject(value)) {
|
|
16
|
+
return {
|
|
17
|
+
...previousValue,
|
|
18
|
+
...value,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
return previousValue
|
|
24
|
+
}, {})
|
|
25
|
+
}
|