@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.
Files changed (55) hide show
  1. package/docs/url_meta/advanced.md +28 -0
  2. package/docs/url_meta/demo/apply_associations.mjs +27 -0
  3. package/docs/url_meta/demo/full_match.mjs +7 -0
  4. package/docs/url_meta/demo/partial_match.mjs +7 -0
  5. package/docs/url_meta/demo/resolve_associations.mjs +12 -0
  6. package/docs/url_meta/notes.md +26 -0
  7. package/docs/url_meta/pattern_matching.md +71 -0
  8. package/docs/url_meta/readme.md +87 -0
  9. package/main.js +27 -0
  10. package/package.json +28 -0
  11. package/src/caller_position.js +20 -0
  12. package/src/data_url.js +50 -0
  13. package/src/inline_content_url_generator.js +21 -0
  14. package/src/meta/aliases.js +28 -0
  15. package/src/meta/as_flat_associations.js +28 -0
  16. package/src/meta/assertions.js +57 -0
  17. package/src/meta/associations.js +25 -0
  18. package/src/meta/pattern_matching.js +245 -0
  19. package/src/meta/resolve_associations.js +25 -0
  20. package/src/meta/url_child_may_match.js +47 -0
  21. package/src/meta/url_meta.js +13 -0
  22. package/src/url_trace.js +229 -0
  23. package/src/url_utils.js +107 -0
  24. package/src/windows_file_path_utils.js +16 -0
  25. package/test/data_url.test.mjs +57 -0
  26. package/test/meta/aliasing.test.mjs +47 -0
  27. package/test/meta/as_flat_associations/basic.test.mjs +64 -0
  28. package/test/meta/assertions.test.mjs +23 -0
  29. package/test/meta/get_url_meta/any.test.mjs +94 -0
  30. package/test/meta/get_url_meta/basic.test.mjs +226 -0
  31. package/test/meta/get_url_meta/complex.test.mjs +203 -0
  32. package/test/meta/get_url_meta/double_wildcard_after.test.mjs +68 -0
  33. package/test/meta/get_url_meta/double_wildcard_around.test.mjs +42 -0
  34. package/test/meta/get_url_meta/double_wildcard_before.test.mjs +55 -0
  35. package/test/meta/get_url_meta/inheritance.test.mjs +59 -0
  36. package/test/meta/is_plain_object.test.mjs +27 -0
  37. package/test/meta/pattern_matching/basic.test.mjs +100 -0
  38. package/test/meta/pattern_matching/common.test.mjs +145 -0
  39. package/test/meta/pattern_matching/complex.test.mjs +159 -0
  40. package/test/meta/pattern_matching/contains.test.mjs +46 -0
  41. package/test/meta/pattern_matching/doublewildcard_after.test.mjs +81 -0
  42. package/test/meta/pattern_matching/doublewildcard_around.test.mjs +42 -0
  43. package/test/meta/pattern_matching/doublewildcard_before.test.mjs +68 -0
  44. package/test/meta/pattern_matching/doublewildcard_inside.test.mjs +42 -0
  45. package/test/meta/pattern_matching/trailing_slash.test.mjs +55 -0
  46. package/test/meta/pattern_matching/wildcard_after.test.mjs +68 -0
  47. package/test/meta/pattern_matching/wildcard_around.test.mjs +94 -0
  48. package/test/meta/pattern_matching/wildcard_before.test.mjs +55 -0
  49. package/test/meta/pattern_matching/wildcard_inside.test.mjs +68 -0
  50. package/test/meta/resolve_associations/basic.test.mjs +42 -0
  51. package/test/meta/resolve_associations/http_pattern.test.mjs +35 -0
  52. package/test/meta/url_child_may_match/inheritance.test.mjs +71 -0
  53. package/test/meta/url_child_may_match/url_child_may_match.test.mjs +166 -0
  54. package/test/url_trace.test.mjs +224 -0
  55. 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,7 @@
1
+ import { URL_META } from "@jsenv/urls"
2
+
3
+ const fullMatch = URL_META.applyPatternMatching({
4
+ pattern: "file:///**/*",
5
+ url: "file:///Users/directory/file.js",
6
+ })
7
+ console.log(JSON.stringify(fullMatch, null, " "))
@@ -0,0 +1,7 @@
1
+ import { URL_META } from "@jsenv/urls"
2
+
3
+ const partialMatch = URL_META.applyPatternMatching({
4
+ pattern: "file:///*.js",
5
+ url: "file:///file.jsx",
6
+ })
7
+ console.log(JSON.stringify(partialMatch, null, " "))
@@ -0,0 +1,12 @@
1
+ import { URL_META } from "@jsenv/urls"
2
+
3
+ const associations = URL_META.resolveAssociations(
4
+ {
5
+ visible: {
6
+ "**/*/": true,
7
+ "**/.git/": false,
8
+ },
9
+ },
10
+ "file:///Users/directory/",
11
+ )
12
+ console.log(JSON.stringify(associations, null, " "))
@@ -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
+ }
@@ -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
+ }