@jskit-ai/http-web 0.1.1

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 (110) hide show
  1. package/fixtures/crud-list-date-filters/App.vue +107 -0
  2. package/fixtures/crud-list-date-filters/index.html +12 -0
  3. package/fixtures/crud-list-date-filters/main.js +24 -0
  4. package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
  5. package/package.json +150 -0
  6. package/src/client/bulkActions.js +47 -0
  7. package/src/client/components/CrudAddEditScreen.vue +186 -0
  8. package/src/client/components/CrudDeleteAction.vue +93 -0
  9. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  10. package/src/client/components/CrudListDateFilterControl.vue +188 -0
  11. package/src/client/components/CrudListFilterSurface.vue +388 -0
  12. package/src/client/components/CrudListRecordActionMenu.vue +120 -0
  13. package/src/client/components/CrudListScreen.vue +501 -0
  14. package/src/client/components/CrudViewScreen.vue +197 -0
  15. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  16. package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
  17. package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
  18. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
  19. package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
  20. package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
  21. package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
  22. package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
  23. package/src/client/composables/internal/useOperationScope.js +144 -0
  24. package/src/client/composables/records/useAddEdit.js +224 -0
  25. package/src/client/composables/records/useCrudAddEdit.js +227 -0
  26. package/src/client/composables/records/useCrudList.js +101 -0
  27. package/src/client/composables/records/useCrudView.js +47 -0
  28. package/src/client/composables/records/useList.js +500 -0
  29. package/src/client/composables/records/useView.js +164 -0
  30. package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
  31. package/src/client/composables/runtime/listUiRuntime.js +123 -0
  32. package/src/client/composables/runtime/modelStateHelpers.js +90 -0
  33. package/src/client/composables/runtime/operationAdapters.js +34 -0
  34. package/src/client/composables/runtime/operationUiHelpers.js +125 -0
  35. package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
  36. package/src/client/composables/runtime/useAddEditCore.js +112 -0
  37. package/src/client/composables/runtime/useCommandCore.js +123 -0
  38. package/src/client/composables/runtime/useEndpointResource.js +199 -0
  39. package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
  40. package/src/client/composables/runtime/useListCore.js +112 -0
  41. package/src/client/composables/runtime/useUiFeedback.js +98 -0
  42. package/src/client/composables/runtime/useViewCore.js +93 -0
  43. package/src/client/composables/runtime/viewUiRuntime.js +96 -0
  44. package/src/client/composables/support/errorMessageHelpers.js +66 -0
  45. package/src/client/composables/support/listQueryParamSupport.js +473 -0
  46. package/src/client/composables/support/listSearchSupport.js +70 -0
  47. package/src/client/composables/support/refValueHelpers.js +19 -0
  48. package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
  49. package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
  50. package/src/client/composables/support/routeTemplateHelpers.js +285 -0
  51. package/src/client/composables/support/scopeHelpers.js +145 -0
  52. package/src/client/composables/useAccess.js +109 -0
  53. package/src/client/composables/useCommand.js +118 -0
  54. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  55. package/src/client/composables/useCrudDeleteAction.js +160 -0
  56. package/src/client/composables/useCrudListBulkActions.js +147 -0
  57. package/src/client/composables/useCrudListFilterLookups.js +146 -0
  58. package/src/client/composables/useCrudListFilters.js +339 -0
  59. package/src/client/composables/useCrudListParentTitle.js +151 -0
  60. package/src/client/composables/useCrudListRowActions.js +144 -0
  61. package/src/client/composables/useCrudListScreen.js +237 -0
  62. package/src/client/composables/useCrudViewScreen.js +77 -0
  63. package/src/client/composables/usePagedCollection.js +181 -0
  64. package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
  65. package/src/client/composables/useScopeRuntime.js +135 -0
  66. package/src/client/filters.js +15 -0
  67. package/src/client/index.js +24 -0
  68. package/src/client/lib/httpClient.js +47 -0
  69. package/src/client/lib/permissions.js +34 -0
  70. package/src/client/rowActions.js +47 -0
  71. package/src/client/support/contractGuards.js +34 -0
  72. package/src/client/support/crudListDateFilterSupport.js +91 -0
  73. package/test/addEditUiRuntime.test.js +110 -0
  74. package/test/crudBindingSupport.test.js +110 -0
  75. package/test/crudHttpClientSupport.test.js +77 -0
  76. package/test/crudJsonApiTransportSupport.test.js +179 -0
  77. package/test/crudListBulkActionSurface.test.js +27 -0
  78. package/test/crudListDateFilterSupport.test.js +92 -0
  79. package/test/crudListDateFilterSurface.browser.test.js +182 -0
  80. package/test/crudListFilterSurface.test.js +90 -0
  81. package/test/crudLookupFieldRuntime.test.js +264 -0
  82. package/test/crudScreenComponents.test.js +110 -0
  83. package/test/errorIntentContract.test.js +31 -0
  84. package/test/errorMessageHelpers.test.js +28 -0
  85. package/test/exportsContract.test.js +68 -0
  86. package/test/listQueryParamSupport.test.js +223 -0
  87. package/test/listUiRuntime.test.js +102 -0
  88. package/test/operationValidationHelpers.test.js +64 -0
  89. package/test/packageBoundary.test.js +31 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/refValueHelpers.test.js +14 -0
  92. package/test/requestTransportOptions.test.js +457 -0
  93. package/test/resourceLoadStateHelpers.test.js +189 -0
  94. package/test/routeTemplateHelpers.test.js +85 -0
  95. package/test/scopeHelpers.test.js +57 -0
  96. package/test/useAddEditCore.test.js +124 -0
  97. package/test/useAddEditRequestQueryParams.test.js +190 -0
  98. package/test/useCrudAddEdit.test.js +415 -0
  99. package/test/useCrudDeleteAction.test.js +139 -0
  100. package/test/useCrudListBulkActions.test.js +65 -0
  101. package/test/useCrudListFilterLookups.test.js +114 -0
  102. package/test/useCrudListFilters.test.js +293 -0
  103. package/test/useCrudListParentTitle.test.js +262 -0
  104. package/test/useCrudListRowActions.test.js +77 -0
  105. package/test/useListRouteInitialization.test.js +337 -0
  106. package/test/useListSearchSupport.test.js +61 -0
  107. package/test/usePagedCollection.test.js +53 -0
  108. package/test/useViewRequestQueryParams.test.js +20 -0
  109. package/test/viewCoreLoading.test.js +44 -0
  110. package/test/viewUiRuntime.test.js +95 -0
@@ -0,0 +1,19 @@
1
+ import { unref } from "vue";
2
+
3
+ function resolveEnabledRef(value) {
4
+ if (value === undefined) {
5
+ return true;
6
+ }
7
+
8
+ if (typeof value === "function") {
9
+ return Boolean(value());
10
+ }
11
+
12
+ return Boolean(unref(value));
13
+ }
14
+
15
+ function resolveTextRef(value) {
16
+ return String(unref(value) || "").trim();
17
+ }
18
+
19
+ export { resolveEnabledRef, resolveTextRef };
@@ -0,0 +1,141 @@
1
+ import { computed, unref } from "vue";
2
+ import {
3
+ buildJsonApiFieldsetsToken,
4
+ normalizeJsonApiFieldsets
5
+ } from "@jskit-ai/kernel/shared/support/jsonApiFieldsets";
6
+ import {
7
+ resolveQueryParamDescriptors,
8
+ resolveActiveQueryParamEntries,
9
+ buildQueryParamEntriesToken
10
+ } from "./listQueryParamSupport.js";
11
+
12
+ function resolveRequestQueryContext(context = null) {
13
+ const source = unref(context);
14
+ return source && typeof source === "object" && !Array.isArray(source) ? source : {};
15
+ }
16
+
17
+ function resolveRequestQueryBaseKey(sourceQueryKey = null) {
18
+ const source = unref(sourceQueryKey);
19
+ if (Array.isArray(source)) {
20
+ return [...source];
21
+ }
22
+ if (source == null) {
23
+ return [];
24
+ }
25
+ return [source];
26
+ }
27
+
28
+ function resolveRequestFieldsets(requestFieldsets = null, context = {}) {
29
+ const source = typeof requestFieldsets === "function"
30
+ ? requestFieldsets(context)
31
+ : unref(requestFieldsets);
32
+ return normalizeJsonApiFieldsets(source);
33
+ }
34
+
35
+ function appendRequestQueryValue(target = {}, key = "", values = []) {
36
+ const normalizedKey = String(key || "").trim();
37
+ const normalizedValues = (Array.isArray(values) ? values : [])
38
+ .map((value) => String(value ?? "").trim())
39
+ .filter(Boolean);
40
+ if (!normalizedKey || normalizedValues.length < 1) {
41
+ return;
42
+ }
43
+
44
+ const currentValue = target[normalizedKey];
45
+ if (currentValue === undefined) {
46
+ target[normalizedKey] = normalizedValues.length === 1 ? normalizedValues[0] : [...normalizedValues];
47
+ return;
48
+ }
49
+
50
+ const currentValues = Array.isArray(currentValue) ? [...currentValue] : [currentValue];
51
+ target[normalizedKey] = [...currentValues, ...normalizedValues];
52
+ }
53
+
54
+ function buildRequestQueryObject(entries = [], { fieldsets = null } = {}) {
55
+ const sourceEntries = Array.isArray(entries) ? entries : [];
56
+ const query = {};
57
+
58
+ for (const entry of sourceEntries) {
59
+ appendRequestQueryValue(query, entry?.key, entry?.values);
60
+ }
61
+
62
+ const normalizedFieldsets = normalizeJsonApiFieldsets(fieldsets);
63
+ return Object.freeze({
64
+ ...query,
65
+ ...(Object.keys(normalizedFieldsets).length > 0
66
+ ? { fields: normalizedFieldsets }
67
+ : {})
68
+ });
69
+ }
70
+
71
+ function createRequestQueryRuntime({
72
+ requestQueryParams = null,
73
+ requestFieldsets = null,
74
+ context = null,
75
+ sourceQueryKey = null
76
+ } = {}) {
77
+ const requestQueryParamDescriptors = computed(() => {
78
+ return resolveQueryParamDescriptors(requestQueryParams, resolveRequestQueryContext(context));
79
+ });
80
+ const activeRequestQueryParamEntries = computed(() => {
81
+ return resolveActiveQueryParamEntries(requestQueryParamDescriptors.value);
82
+ });
83
+ const activeRequestQueryParamsToken = computed(() => {
84
+ return buildQueryParamEntriesToken(activeRequestQueryParamEntries.value);
85
+ });
86
+ const activeRequestFieldsets = computed(() => {
87
+ return resolveRequestFieldsets(
88
+ requestFieldsets,
89
+ resolveRequestQueryContext(context)
90
+ );
91
+ });
92
+ const activeRequestFieldsetsToken = computed(() => {
93
+ return buildJsonApiFieldsetsToken(activeRequestFieldsets.value);
94
+ });
95
+ const queryKeyParts = computed(() => {
96
+ const parts = [];
97
+ if (activeRequestQueryParamsToken.value) {
98
+ parts.push("__request_query__", activeRequestQueryParamsToken.value);
99
+ }
100
+ if (activeRequestFieldsetsToken.value) {
101
+ parts.push("__request_fieldsets__", activeRequestFieldsetsToken.value);
102
+ }
103
+ return Object.freeze(parts);
104
+ });
105
+ const queryKey = computed(() => {
106
+ if (queryKeyParts.value.length < 1) {
107
+ return unref(sourceQueryKey);
108
+ }
109
+
110
+ return [
111
+ ...resolveRequestQueryBaseKey(sourceQueryKey),
112
+ ...queryKeyParts.value
113
+ ];
114
+ });
115
+ const requestQuery = computed(() => {
116
+ const query = buildRequestQueryObject(activeRequestQueryParamEntries.value, {
117
+ fieldsets: activeRequestFieldsets.value
118
+ });
119
+ if (Object.keys(query).length < 1) {
120
+ return null;
121
+ }
122
+
123
+ return query;
124
+ });
125
+
126
+ return Object.freeze({
127
+ requestQueryParamDescriptors,
128
+ activeRequestQueryParamEntries,
129
+ activeRequestQueryParamsToken,
130
+ activeRequestFieldsets,
131
+ activeRequestFieldsetsToken,
132
+ queryKeyParts,
133
+ queryKey,
134
+ requestQuery
135
+ });
136
+ }
137
+
138
+ export {
139
+ buildRequestQueryObject,
140
+ createRequestQueryRuntime
141
+ };
@@ -0,0 +1,208 @@
1
+ import { unref } from "vue";
2
+
3
+ function hasResolvedQueryData({ query = null, data = null } = {}) {
4
+ const querySucceeded = Boolean(unref(query?.isSuccess));
5
+ const hasDataPayload = unref(data) != null;
6
+
7
+ return querySucceeded || hasDataPayload;
8
+ }
9
+
10
+ function mergeQueryMeta(queryOptions = null, meta = {}) {
11
+ const sourceOptions =
12
+ queryOptions && typeof queryOptions === "object" && !Array.isArray(queryOptions) ? queryOptions : {};
13
+ const sourceMeta =
14
+ sourceOptions.meta && typeof sourceOptions.meta === "object" && !Array.isArray(sourceOptions.meta)
15
+ ? sourceOptions.meta
16
+ : {};
17
+ const sourceJskitMeta =
18
+ sourceMeta.jskit && typeof sourceMeta.jskit === "object" && !Array.isArray(sourceMeta.jskit)
19
+ ? sourceMeta.jskit
20
+ : {};
21
+ const nextJskitMeta =
22
+ meta.jskit && typeof meta.jskit === "object" && !Array.isArray(meta.jskit)
23
+ ? meta.jskit
24
+ : {};
25
+
26
+ return {
27
+ ...sourceOptions,
28
+ meta: {
29
+ ...sourceMeta,
30
+ ...meta,
31
+ jskit: {
32
+ ...sourceJskitMeta,
33
+ ...nextJskitMeta
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ function normalizeText(value = "") {
40
+ return String(value || "").trim();
41
+ }
42
+
43
+ function firstNormalizedText(...values) {
44
+ for (const value of values) {
45
+ const normalized = normalizeText(value);
46
+ if (normalized) {
47
+ return normalized;
48
+ }
49
+ }
50
+ return "";
51
+ }
52
+
53
+ function firstNonNegativeFiniteNumber(...values) {
54
+ for (const value of values) {
55
+ const numberValue = Number(value);
56
+ if (Number.isFinite(numberValue)) {
57
+ return Math.max(0, numberValue);
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function normalizePlainObject(value = null) {
64
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ }
66
+
67
+ function normalizeRequestRecoveryInput(requestRecovery = null) {
68
+ if (typeof requestRecovery === "string") {
69
+ return {
70
+ label: requestRecovery
71
+ };
72
+ }
73
+ return normalizePlainObject(requestRecovery);
74
+ }
75
+
76
+ function normalizeRequestRecoveryMethod(value = "") {
77
+ return normalizeText(value).toUpperCase();
78
+ }
79
+
80
+ function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}) {
81
+ return sourceJskitMeta.requestRecovery === false;
82
+ }
83
+
84
+ function hasUsableMetaValue(source = {}, key = "") {
85
+ return Object.hasOwn(source, key) && source[key] !== undefined && source[key] !== null && source[key] !== "";
86
+ }
87
+
88
+ function buildRequestRecoveryMeta(requestRecovery = null, defaults = {}) {
89
+ if (requestRecovery === false) {
90
+ return {
91
+ jskit: {
92
+ requestRecovery: false
93
+ }
94
+ };
95
+ }
96
+
97
+ const fallback = normalizePlainObject(defaults);
98
+ const source = normalizeRequestRecoveryInput(requestRecovery);
99
+ if (source.requestRecovery === false || source.enabled === false) {
100
+ return {
101
+ jskit: {
102
+ requestRecovery: false
103
+ }
104
+ };
105
+ }
106
+
107
+ const label = firstNormalizedText(
108
+ source.requestRecoveryLabel,
109
+ source.label,
110
+ fallback.requestRecoveryLabel,
111
+ fallback.label
112
+ );
113
+ const recoverySource = firstNormalizedText(
114
+ source.requestRecoverySource,
115
+ source.source,
116
+ fallback.requestRecoverySource,
117
+ fallback.source
118
+ );
119
+ const dedupeKey = firstNormalizedText(
120
+ source.requestRecoveryDedupeKey,
121
+ source.dedupeKey,
122
+ fallback.requestRecoveryDedupeKey,
123
+ fallback.dedupeKey
124
+ );
125
+ const method = normalizeRequestRecoveryMethod(
126
+ firstNormalizedText(
127
+ source.requestRecoveryMethod,
128
+ source.method,
129
+ fallback.requestRecoveryMethod,
130
+ fallback.method
131
+ )
132
+ );
133
+ const dedupeWindowMs = firstNonNegativeFiniteNumber(
134
+ source.requestRecoveryDedupeWindowMs,
135
+ source.dedupeWindowMs,
136
+ fallback.requestRecoveryDedupeWindowMs,
137
+ fallback.dedupeWindowMs
138
+ );
139
+
140
+ const jskit = {};
141
+ if (label) {
142
+ jskit.requestRecoveryLabel = label;
143
+ }
144
+ if (recoverySource) {
145
+ jskit.requestRecoverySource = recoverySource;
146
+ }
147
+ if (dedupeKey) {
148
+ jskit.requestRecoveryDedupeKey = dedupeKey;
149
+ }
150
+ if (method) {
151
+ jskit.requestRecoveryMethod = method;
152
+ }
153
+ if (dedupeWindowMs !== null) {
154
+ jskit.requestRecoveryDedupeWindowMs = dedupeWindowMs;
155
+ }
156
+
157
+ return Object.keys(jskit).length > 0 ? { jskit } : {};
158
+ }
159
+
160
+ function resolveRequestRecoveryDefaults(queryOptions = null, defaults = {}) {
161
+ const sourceOptions = normalizePlainObject(queryOptions);
162
+ const sourceMeta = normalizePlainObject(sourceOptions.meta);
163
+ const sourceJskitMeta = normalizePlainObject(sourceMeta.jskit);
164
+ const fallback = normalizePlainObject(defaults);
165
+ if (isRequestRecoveryMetaDisabled(sourceJskitMeta)) {
166
+ return {};
167
+ }
168
+
169
+ const hasExplicitLabel = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryLabel");
170
+ const hasExplicitSource = hasUsableMetaValue(sourceJskitMeta, "requestRecoverySource");
171
+ const hasExplicitDedupeKey = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeKey");
172
+ const hasExplicitDedupeWindowMs = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeWindowMs");
173
+ const hasExplicitMethod = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryMethod");
174
+
175
+ return {
176
+ ...fallback,
177
+ requestRecoveryLabel: hasExplicitLabel ? "" : fallback.requestRecoveryLabel,
178
+ label: hasExplicitLabel ? "" : fallback.label,
179
+ requestRecoverySource: hasExplicitSource ? "" : fallback.requestRecoverySource,
180
+ source: hasExplicitSource ? "" : fallback.source,
181
+ requestRecoveryDedupeKey: hasExplicitDedupeKey ? "" : fallback.requestRecoveryDedupeKey,
182
+ dedupeKey: hasExplicitDedupeKey ? "" : fallback.dedupeKey,
183
+ requestRecoveryDedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.requestRecoveryDedupeWindowMs,
184
+ dedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.dedupeWindowMs,
185
+ requestRecoveryMethod: hasExplicitMethod ? "" : fallback.requestRecoveryMethod,
186
+ method: hasExplicitMethod ? "" : fallback.method
187
+ };
188
+ }
189
+
190
+ function mergeRequestRecoveryQueryMeta(queryOptions = null, requestRecovery = null, defaults = {}) {
191
+ const sourceOptions = normalizePlainObject(queryOptions);
192
+ const effectiveDefaults =
193
+ requestRecovery == null
194
+ ? resolveRequestRecoveryDefaults(sourceOptions, defaults)
195
+ : normalizePlainObject(defaults);
196
+ const meta = buildRequestRecoveryMeta(requestRecovery, effectiveDefaults);
197
+ if (Object.keys(meta).length < 1) {
198
+ return sourceOptions;
199
+ }
200
+ return mergeQueryMeta(sourceOptions, meta);
201
+ }
202
+
203
+ export {
204
+ buildRequestRecoveryMeta,
205
+ hasResolvedQueryData,
206
+ mergeQueryMeta,
207
+ mergeRequestRecoveryQueryMeta
208
+ };
@@ -0,0 +1,285 @@
1
+ import { unref } from "vue";
2
+ import { asPlainObject } from "./scopeHelpers.js";
3
+
4
+ const ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
5
+
6
+ function normalizeRouteParamName(value = "", { context = "http-web route param" } = {}) {
7
+ const normalizedValue = String(value || "").trim();
8
+ if (!normalizedValue) {
9
+ throw new TypeError(`${context} must be a non-empty route parameter name.`);
10
+ }
11
+ if (!ROUTE_PARAM_NAME_PATTERN.test(normalizedValue)) {
12
+ throw new TypeError(
13
+ `${context} "${normalizedValue}" is invalid. Use letters, numbers, and underscores only.`
14
+ );
15
+ }
16
+
17
+ return normalizedValue;
18
+ }
19
+
20
+ function toRouteParamValue(value) {
21
+ if (Array.isArray(value)) {
22
+ return toRouteParamValue(value[0]);
23
+ }
24
+ if (value == null) {
25
+ return "";
26
+ }
27
+
28
+ return String(value).trim();
29
+ }
30
+
31
+ function resolveRouteSourceValue(source = null) {
32
+ if (typeof source === "function") {
33
+ return source();
34
+ }
35
+
36
+ return unref(source);
37
+ }
38
+
39
+ function resolveRouteParamsSource(source = null) {
40
+ return asPlainObject(resolveRouteSourceValue(source));
41
+ }
42
+
43
+ function normalizeRouteParamNameList(value = []) {
44
+ const source = Array.isArray(value) ? value : [];
45
+ return source
46
+ .map((entry) => String(entry || "").trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function resolveRouteParamNamesSource(source = []) {
51
+ return normalizeRouteParamNameList(resolveRouteSourceValue(source));
52
+ }
53
+
54
+ function normalizeRoutePathname(value = "") {
55
+ const rawPathname = String(value || "").trim();
56
+ const sanitizedPathname = rawPathname.split(/[?#]/u, 1)[0] || "";
57
+ if (!sanitizedPathname) {
58
+ return "/";
59
+ }
60
+
61
+ return sanitizedPathname.startsWith("/") ? sanitizedPathname : `/${sanitizedPathname}`;
62
+ }
63
+
64
+ function resolveRoutePathnameSource(source = "") {
65
+ return normalizeRoutePathname(resolveRouteSourceValue(source));
66
+ }
67
+
68
+ function segmentMatchesParamValue(segment = "", paramValue = "") {
69
+ const normalizedSegment = String(segment || "").trim();
70
+ const normalizedParamValue = toRouteParamValue(paramValue);
71
+ if (!normalizedSegment || !normalizedParamValue) {
72
+ return false;
73
+ }
74
+
75
+ const encodedParamValue = encodeURIComponent(normalizedParamValue);
76
+ if (normalizedSegment === encodedParamValue) {
77
+ return true;
78
+ }
79
+
80
+ try {
81
+ return decodeURIComponent(normalizedSegment) === normalizedParamValue;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function findRouteParamSegmentIndex(segments = [], paramValue = "", fromIndex = 0) {
88
+ const source = Array.isArray(segments) ? segments : [];
89
+ const cursor = Number.isInteger(fromIndex) && fromIndex > 0 ? fromIndex : 0;
90
+ for (let index = cursor; index < source.length; index += 1) {
91
+ if (segmentMatchesParamValue(source[index], paramValue)) {
92
+ return index;
93
+ }
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ function normalizePathPrefix(segments = [], endIndex = -1) {
99
+ const source = Array.isArray(segments) ? segments : [];
100
+ if (!Number.isInteger(endIndex) || endIndex < 0) {
101
+ return "/";
102
+ }
103
+ return `/${source.slice(0, endIndex + 1).join("/")}`;
104
+ }
105
+
106
+ function resolveAnchorEndIndex(segmentIndex = -1, totalSegments = 0, anchorMode = "at") {
107
+ const normalizedSegmentIndex = Number.isInteger(segmentIndex) ? segmentIndex : -1;
108
+ if (normalizedSegmentIndex < 0) {
109
+ return -1;
110
+ }
111
+
112
+ const normalizedTotalSegments = Number.isInteger(totalSegments) && totalSegments > 0 ? totalSegments : 0;
113
+ const normalizedMode = String(anchorMode || "at").trim().toLowerCase();
114
+ if (normalizedMode === "before") {
115
+ return normalizedSegmentIndex - 1;
116
+ }
117
+ if (normalizedMode === "after") {
118
+ return normalizedTotalSegments > 0
119
+ ? Math.min(normalizedSegmentIndex + 1, normalizedTotalSegments - 1)
120
+ : normalizedSegmentIndex + 1;
121
+ }
122
+
123
+ return normalizedSegmentIndex;
124
+ }
125
+
126
+ function resolveScopedRoutePathname({
127
+ currentPathname = "/",
128
+ params = {},
129
+ orderedParamNames = [],
130
+ anchorParamName = "",
131
+ anchorParamValue = "",
132
+ anchorMode = "at"
133
+ } = {}) {
134
+ const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
135
+ const normalizedAnchorParamName = String(anchorParamName || "").trim();
136
+ if (!normalizedAnchorParamName) {
137
+ return normalizedCurrentPathname;
138
+ }
139
+
140
+ const sourceParams = asPlainObject(params);
141
+ const segments = normalizedCurrentPathname.split("/").filter(Boolean);
142
+ if (segments.length < 1) {
143
+ return normalizedCurrentPathname;
144
+ }
145
+
146
+ const paramNames = resolveRouteParamNamesSource(orderedParamNames);
147
+ let cursor = 0;
148
+ for (const paramName of paramNames) {
149
+ const segmentIndex = findRouteParamSegmentIndex(segments, sourceParams[paramName], cursor);
150
+ if (segmentIndex < 0) {
151
+ continue;
152
+ }
153
+
154
+ if (paramName === normalizedAnchorParamName) {
155
+ const endIndex = resolveAnchorEndIndex(segmentIndex, segments.length, anchorMode);
156
+ return normalizePathPrefix(segments, endIndex);
157
+ }
158
+
159
+ cursor = segmentIndex + 1;
160
+ }
161
+
162
+ const fallbackAnchorValue = toRouteParamValue(anchorParamValue) ||
163
+ toRouteParamValue(sourceParams[normalizedAnchorParamName]);
164
+ if (!fallbackAnchorValue) {
165
+ return normalizedCurrentPathname;
166
+ }
167
+
168
+ const fallbackSegmentIndex = findRouteParamSegmentIndex(segments, fallbackAnchorValue, 0);
169
+ if (fallbackSegmentIndex < 0) {
170
+ return normalizedCurrentPathname;
171
+ }
172
+
173
+ const fallbackEndIndex = resolveAnchorEndIndex(fallbackSegmentIndex, segments.length, anchorMode);
174
+ return normalizePathPrefix(segments, fallbackEndIndex);
175
+ }
176
+
177
+ function resolveRouteTemplatePath(routeTemplate = "", params = {}) {
178
+ const normalizedTemplate = String(routeTemplate || "").trim();
179
+ if (!normalizedTemplate) {
180
+ return "";
181
+ }
182
+
183
+ const source = asPlainObject(params);
184
+ const missingParams = [];
185
+ const resolvedPath = normalizedTemplate.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_, key) => {
186
+ const value = toRouteParamValue(source[key]);
187
+ if (!value) {
188
+ missingParams.push(key);
189
+ return `:${key}`;
190
+ }
191
+
192
+ return encodeURIComponent(value);
193
+ });
194
+
195
+ if (missingParams.length > 0) {
196
+ return "";
197
+ }
198
+
199
+ return resolvedPath;
200
+ }
201
+
202
+ function resolveRouteTemplateLocation(routeTemplate = "", { params = {}, currentPathname = "/" } = {}) {
203
+ const resolvedTemplatePath = resolveRouteTemplatePath(routeTemplate, params);
204
+ if (!resolvedTemplatePath) {
205
+ return "";
206
+ }
207
+ if (resolvedTemplatePath.startsWith("/")) {
208
+ return resolvedTemplatePath;
209
+ }
210
+
211
+ const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
212
+ const basePathname = normalizedCurrentPathname.endsWith("/")
213
+ ? normalizedCurrentPathname
214
+ : `${normalizedCurrentPathname}/`;
215
+ const resolvedPathname = new URL(resolvedTemplatePath, `https://jskit.local${basePathname}`).pathname;
216
+ if (resolvedPathname.length > 1 && resolvedPathname.endsWith("/")) {
217
+ return resolvedPathname.slice(0, -1);
218
+ }
219
+
220
+ return resolvedPathname;
221
+ }
222
+
223
+ function extractRouteParamNames(pathTemplate = "") {
224
+ const normalizedTemplate = String(pathTemplate || "").trim();
225
+ if (!normalizedTemplate) {
226
+ return [];
227
+ }
228
+
229
+ const names = [];
230
+ const seen = new Set();
231
+ const pattern = /:([A-Za-z][A-Za-z0-9_]*)/g;
232
+ let match = null;
233
+ while ((match = pattern.exec(normalizedTemplate)) != null) {
234
+ const name = String(match[1] || "").trim();
235
+ if (!name || seen.has(name)) {
236
+ continue;
237
+ }
238
+ seen.add(name);
239
+ names.push(name);
240
+ }
241
+
242
+ return names;
243
+ }
244
+
245
+ function resolveRouteParamNamesInOrder(route = null) {
246
+ const sourceRoute = route && typeof route === "object" ? route : {};
247
+ const matched = Array.isArray(sourceRoute.matched) ? sourceRoute.matched : [];
248
+ const names = [];
249
+ const seen = new Set();
250
+
251
+ for (const entry of matched) {
252
+ const entryPath = String(entry?.path || "").trim();
253
+ if (!entryPath) {
254
+ continue;
255
+ }
256
+ for (const name of extractRouteParamNames(entryPath)) {
257
+ if (seen.has(name)) {
258
+ continue;
259
+ }
260
+ seen.add(name);
261
+ names.push(name);
262
+ }
263
+ }
264
+
265
+ if (names.length > 0) {
266
+ return names;
267
+ }
268
+
269
+ return Object.keys(asPlainObject(sourceRoute.params))
270
+ .map((entry) => String(entry || "").trim())
271
+ .filter(Boolean);
272
+ }
273
+
274
+ export {
275
+ normalizeRouteParamName,
276
+ toRouteParamValue,
277
+ resolveRouteParamsSource,
278
+ resolveRouteParamNamesSource,
279
+ resolveRoutePathnameSource,
280
+ resolveScopedRoutePathname,
281
+ resolveRouteTemplatePath,
282
+ resolveRouteTemplateLocation,
283
+ extractRouteParamNames,
284
+ resolveRouteParamNamesInOrder
285
+ };