@hybridly/vue 0.0.1-dev.2

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/dist/index.mjs ADDED
@@ -0,0 +1,782 @@
1
+ import { ref, shallowRef, unref, triggerRef, defineComponent, h, isRef, reactive, readonly, computed, toRaw, watch } from 'vue';
2
+ import { registerHook, registerHookOnce, createRouter, makeUrl, router } from '@hybridly/core';
3
+ export { router } from '@hybridly/core';
4
+ import { debug, showPageComponentErrorModal, merge } from '@hybridly/utils';
5
+ import { progress } from '@hybridly/progress-plugin';
6
+ import { setupDevtoolsPlugin } from '@vue/devtools-api';
7
+ import qs, { parse, stringify } from 'qs';
8
+ import isEqual from 'lodash.isequal';
9
+ import clone from 'lodash.clonedeep';
10
+
11
+ const state = {
12
+ context: ref(),
13
+ view: shallowRef(),
14
+ viewLayout: shallowRef(),
15
+ viewKey: ref(),
16
+ dialog: shallowRef(),
17
+ dialogKey: ref(),
18
+ routes: ref(),
19
+ setRoutes(routes) {
20
+ debug.adapter("vue:state:routes", "Setting routes:", routes);
21
+ if (routes) {
22
+ state.routes.value = unref(routes);
23
+ }
24
+ },
25
+ setView(view) {
26
+ debug.adapter("vue:state:view", "Setting view:", view);
27
+ state.view.value = view;
28
+ },
29
+ setViewLayout(layout) {
30
+ debug.adapter("vue:state:view", "Setting layout:", layout);
31
+ state.viewLayout.value = layout;
32
+ },
33
+ setDialog(dialog) {
34
+ debug.adapter("vue:state:dialog", "Setting dialog:", dialog);
35
+ state.dialog.value = dialog;
36
+ },
37
+ setContext(context) {
38
+ debug.adapter("vue:state:context", "Setting context:", context);
39
+ state.context.value = unref(context);
40
+ triggerRef(state.context);
41
+ },
42
+ setViewKey(key) {
43
+ debug.adapter("vue:state:key", "Setting view key:", key);
44
+ state.viewKey.value = unref(key);
45
+ },
46
+ setDialogKey(key) {
47
+ debug.adapter("vue:state:key", "Setting dialog key:", key);
48
+ state.dialogKey.value = unref(key);
49
+ }
50
+ };
51
+
52
+ const wrapper = defineComponent({
53
+ name: "Hybridly",
54
+ setup(props) {
55
+ if (typeof window !== "undefined") {
56
+ state.setContext(props.context);
57
+ if (!props.context) {
58
+ throw new Error("Hybridly was not properly initialized. The context is missing.");
59
+ }
60
+ }
61
+ function renderLayout(child) {
62
+ debug.adapter("vue:render:layout", "Rendering layout.");
63
+ if (typeof state.view.value?.layout === "function") {
64
+ return state.view.value.layout(h, child);
65
+ }
66
+ if (Array.isArray(state.view.value?.layout)) {
67
+ return state.view.value.layout.concat(child).reverse().reduce((child2, layout) => {
68
+ layout.inheritAttrs = !!layout.inheritAttrs;
69
+ return h(layout, { ...state.context.value.view.properties }, () => child2);
70
+ });
71
+ }
72
+ return [
73
+ h(state.view.value?.layout, { ...state.context.value.view.properties }, () => child),
74
+ renderDialog()
75
+ ];
76
+ }
77
+ function renderView() {
78
+ debug.adapter("vue:render:view", "Rendering view.");
79
+ state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
80
+ return h(state.view.value, {
81
+ ...state.context.value.view.properties,
82
+ key: state.viewKey.value
83
+ });
84
+ }
85
+ function renderDialog() {
86
+ debug.adapter("vue:render:dialog", "Rendering dialog.");
87
+ if (state.dialog.value) {
88
+ return h(state.dialog.value, {
89
+ ...state.dialog.value.properties,
90
+ key: state.dialogKey.value
91
+ });
92
+ }
93
+ }
94
+ return () => {
95
+ if (state.view.value) {
96
+ const view = renderView();
97
+ if (state.viewLayout.value) {
98
+ state.view.value.layout = state.viewLayout.value;
99
+ state.viewLayout.value = void 0;
100
+ }
101
+ if (state.view.value.layout) {
102
+ return renderLayout(view);
103
+ }
104
+ return [view, renderDialog()];
105
+ }
106
+ };
107
+ },
108
+ props: {
109
+ context: {
110
+ type: Object,
111
+ required: true
112
+ }
113
+ }
114
+ });
115
+
116
+ const hybridlyStateType = "hybridly";
117
+ const hybridlyEventsTimelineLayerId = "Hybridly";
118
+ function setupDevtools(app) {
119
+ setupDevtoolsPlugin({
120
+ id: "hybridly",
121
+ label: "Hybridly",
122
+ packageName: "@hybridly/vue",
123
+ homepage: "https://github.com/hybridly",
124
+ app,
125
+ enableEarlyProxy: true,
126
+ componentStateTypes: [
127
+ hybridlyStateType
128
+ ]
129
+ }, (api) => {
130
+ api.on.inspectComponent((payload) => {
131
+ payload.instanceData.state.push({
132
+ type: hybridlyStateType,
133
+ key: "properties",
134
+ value: state.context.value?.view.properties,
135
+ editable: true
136
+ });
137
+ payload.instanceData.state.push({
138
+ type: hybridlyStateType,
139
+ key: "component",
140
+ value: state.context.value?.view.name
141
+ });
142
+ payload.instanceData.state.push({
143
+ type: hybridlyStateType,
144
+ key: "version",
145
+ value: state.context.value?.version
146
+ });
147
+ payload.instanceData.state.push({
148
+ type: hybridlyStateType,
149
+ key: "url",
150
+ value: state.context.value?.url
151
+ });
152
+ payload.instanceData.state.push({
153
+ type: hybridlyStateType,
154
+ key: "router",
155
+ value: state.routes.value
156
+ });
157
+ });
158
+ api.on.editComponentState((payload) => {
159
+ if (payload.type === hybridlyStateType) {
160
+ payload.set(state.context.value?.view);
161
+ }
162
+ });
163
+ api.addTimelineLayer({
164
+ id: hybridlyEventsTimelineLayerId,
165
+ color: 16501221,
166
+ label: "Hybridly"
167
+ });
168
+ const listen = [
169
+ "start",
170
+ "data",
171
+ "navigate",
172
+ "progress",
173
+ "error",
174
+ "abort",
175
+ "success",
176
+ "invalid",
177
+ "exception",
178
+ "fail",
179
+ "after"
180
+ ];
181
+ registerHook("before", (options) => {
182
+ const groupId = (Math.random() + 1).toString(36).substring(7);
183
+ api.addTimelineEvent({
184
+ layerId: hybridlyEventsTimelineLayerId,
185
+ event: {
186
+ groupId,
187
+ title: "before",
188
+ time: api.now(),
189
+ data: options
190
+ }
191
+ });
192
+ listen.forEach((event) => registerHookOnce(event, (data) => {
193
+ api.addTimelineEvent({
194
+ layerId: hybridlyEventsTimelineLayerId,
195
+ event: {
196
+ groupId,
197
+ title: event,
198
+ time: api.now(),
199
+ data
200
+ }
201
+ });
202
+ if (event === "after") {
203
+ setTimeout(() => {
204
+ triggerRef(state.context);
205
+ api.notifyComponentUpdate();
206
+ }, 100);
207
+ }
208
+ }));
209
+ });
210
+ });
211
+ }
212
+ const plugin = {
213
+ install(app) {
214
+ if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) {
215
+ setupDevtools(app);
216
+ }
217
+ }
218
+ };
219
+
220
+ async function initializeHybridly(options) {
221
+ const { element, payload, resolve } = prepare(options);
222
+ if (!element) {
223
+ throw new Error("Could not find an HTML element to initialize Vue on.");
224
+ }
225
+ if (!payload) {
226
+ throw new Error("No payload. Are you using `@hybridly` or the `payload` option?");
227
+ }
228
+ state.setContext(await createRouter({
229
+ plugins: options.plugins,
230
+ serializer: options.serializer,
231
+ adapter: {
232
+ resolveComponent: resolve,
233
+ swapDialog: async () => {
234
+ },
235
+ swapView: async (options2) => {
236
+ state.setView(options2.component);
237
+ if (!options2.preserveState) {
238
+ state.setViewKey(Date.now());
239
+ }
240
+ },
241
+ update: (context) => {
242
+ state.setContext(context);
243
+ }
244
+ },
245
+ payload
246
+ }));
247
+ await options.setup({
248
+ element,
249
+ wrapper,
250
+ hybridly: plugin,
251
+ props: { context: state.context.value },
252
+ render: () => h(wrapper, { context: state.context.value })
253
+ });
254
+ }
255
+ function prepare(options) {
256
+ debug.adapter("vue", "Preparing Hybridly with options:", options);
257
+ const isServer = typeof window === "undefined";
258
+ const id = options.id ?? "root";
259
+ const element = document?.getElementById(id) ?? void 0;
260
+ debug.adapter("vue", `Element "${id}" is:`, element);
261
+ const payload = options.payload ?? element?.dataset.payload ? JSON.parse(element.dataset.payload) : void 0;
262
+ if (options.cleanup !== false) {
263
+ delete element.dataset.payload;
264
+ }
265
+ debug.adapter("vue", "Resolved:", { isServer, element, payload });
266
+ const resolve = async (name) => {
267
+ debug.adapter("vue", "Resolving component", name);
268
+ if (options.resolve) {
269
+ const component = await options.resolve?.(name);
270
+ return component.default ?? component;
271
+ }
272
+ if (options.pages) {
273
+ return await resolvePageComponent(name, options.pages, options.layout);
274
+ }
275
+ throw new Error("Either `initializeHybridly#resolve` or `initializeHybridly#pages` should be defined.");
276
+ };
277
+ if (typeof window !== "undefined") {
278
+ const routes = window.hybridly?.routes;
279
+ if (routes) {
280
+ state.setRoutes(window.hybridly?.routes);
281
+ window.addEventListener("hybridly:routes", (event) => {
282
+ state.setRoutes(event.detail);
283
+ });
284
+ }
285
+ }
286
+ if (options.progress !== false) {
287
+ options.plugins = [
288
+ progress(typeof options.progress === "object" ? options.progress : {}),
289
+ ...options.plugins ?? []
290
+ ];
291
+ }
292
+ return {
293
+ isServer,
294
+ element,
295
+ payload,
296
+ resolve
297
+ };
298
+ }
299
+ async function resolvePageComponent(name, pages, defaultLayout) {
300
+ const path = Object.keys(pages).sort((a, b) => a.length - b.length).find((path2) => path2.endsWith(`${name.replaceAll(".", "/")}.vue`));
301
+ if (!path) {
302
+ showPageComponentErrorModal(name);
303
+ console.warn(`Page component "${name}" could not be found. Available pages:`, Object.keys(pages));
304
+ return;
305
+ }
306
+ let component = typeof pages[path] === "function" ? await pages[path]() : pages[path];
307
+ component = component.default ?? component;
308
+ component.layout ?? (component.layout = defaultLayout);
309
+ return component;
310
+ }
311
+
312
+ const RouterLink = defineComponent({
313
+ name: "RouterLink",
314
+ setup(_, { slots, attrs }) {
315
+ return (props) => {
316
+ let data = props.data ?? {};
317
+ const url = makeUrl(props.href ?? "");
318
+ const method = props.method ?? "GET";
319
+ const as = typeof props.as === "object" ? props.as : props.as?.toLowerCase() ?? "a";
320
+ if (method === "GET") {
321
+ debug.adapter("vue", "Moving data object to URL parameters.");
322
+ url.search = qs.stringify(merge(data, qs.parse(url.search, { ignoreQueryPrefix: true })), {
323
+ encodeValuesOnly: true,
324
+ arrayFormat: "indices"
325
+ });
326
+ data = {};
327
+ }
328
+ if (as === "a" && method !== "GET") {
329
+ debug.adapter("vue", `Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues.
330
+
331
+ Please specify a more appropriate element using the "as" attribute. For example:
332
+
333
+ <Link href="${url}" method="${method}" as="button">...</Link>`);
334
+ }
335
+ return h(props.as, {
336
+ ...attrs,
337
+ ...as === "a" ? { href: url } : {},
338
+ ...props.disabled ? { disabled: props.disabled } : {},
339
+ onClick: (event) => {
340
+ if (props.external) {
341
+ return;
342
+ }
343
+ if (!shouldIntercept(event)) {
344
+ return;
345
+ }
346
+ event.preventDefault();
347
+ if (props.disabled) {
348
+ return;
349
+ }
350
+ router.visit({
351
+ url,
352
+ data,
353
+ method,
354
+ preserveState: method !== "GET",
355
+ ...props.options
356
+ });
357
+ }
358
+ }, slots);
359
+ };
360
+ },
361
+ props: {
362
+ href: {
363
+ type: String,
364
+ required: true
365
+ },
366
+ as: {
367
+ type: [String, Object],
368
+ default: "a"
369
+ },
370
+ method: {
371
+ type: String,
372
+ default: "GET"
373
+ },
374
+ data: {
375
+ type: Object,
376
+ default: () => ({})
377
+ },
378
+ external: {
379
+ type: Boolean,
380
+ default: false
381
+ },
382
+ disabled: {
383
+ type: Boolean,
384
+ default: false
385
+ },
386
+ options: {
387
+ type: Object,
388
+ default: () => ({})
389
+ }
390
+ }
391
+ });
392
+ function shouldIntercept(event) {
393
+ const isLink = event.currentTarget.tagName.toLowerCase() === "a";
394
+ return !(event.target && (event?.target).isContentEditable || event.defaultPrevented || isLink && event.which > 1 || isLink && event.altKey || isLink && event.ctrlKey || isLink && event.metaKey || isLink && event.shiftKey);
395
+ }
396
+
397
+ const HybridlyImports = {
398
+ "hybridly/vue": [
399
+ "useProperty",
400
+ "useProperties",
401
+ "useRouter",
402
+ "useBackForward",
403
+ "useContext",
404
+ "useForm",
405
+ "useHistoryState",
406
+ "usePaginator",
407
+ "useLayout",
408
+ "route"
409
+ ],
410
+ "hybridly": [
411
+ "registerHook",
412
+ "registerHookOnce",
413
+ "router",
414
+ "can"
415
+ ]
416
+ };
417
+
418
+ function HybridlyResolver(options = {}) {
419
+ options = {
420
+ linkName: "RouterLink",
421
+ ...options
422
+ };
423
+ return {
424
+ type: "component",
425
+ resolve: (name) => {
426
+ if (name === options.linkName) {
427
+ return {
428
+ name: "RouterLink",
429
+ as: options.linkName,
430
+ from: "hybridly/vue"
431
+ };
432
+ }
433
+ }
434
+ };
435
+ }
436
+
437
+ function toReactive(objectRef) {
438
+ if (!isRef(objectRef)) {
439
+ return reactive(objectRef);
440
+ }
441
+ const proxy = new Proxy({}, {
442
+ get(_, p, receiver) {
443
+ return unref(Reflect.get(objectRef.value, p, receiver));
444
+ },
445
+ set(_, p, value) {
446
+ if (isRef(objectRef.value[p]) && !isRef(value)) {
447
+ objectRef.value[p].value = value;
448
+ } else {
449
+ objectRef.value[p] = value;
450
+ }
451
+ return true;
452
+ },
453
+ deleteProperty(_, p) {
454
+ return Reflect.deleteProperty(objectRef.value, p);
455
+ },
456
+ has(_, p) {
457
+ return Reflect.has(objectRef.value, p);
458
+ },
459
+ ownKeys() {
460
+ return Object.keys(objectRef.value);
461
+ },
462
+ getOwnPropertyDescriptor() {
463
+ return {
464
+ enumerable: true,
465
+ configurable: true
466
+ };
467
+ }
468
+ });
469
+ return reactive(proxy);
470
+ }
471
+
472
+ function useProperties() {
473
+ return readonly(toReactive(computed(() => state.context.value?.view.properties)));
474
+ }
475
+ function useProperty(path, fallback) {
476
+ return computed(() => path.split(".").reduce((o, i) => o[i], state.context.value?.view.properties) ?? fallback);
477
+ }
478
+
479
+ function useContext() {
480
+ return computed(() => state.context.value);
481
+ }
482
+
483
+ function useRouter() {
484
+ return router;
485
+ }
486
+
487
+ function safeClone(obj) {
488
+ return clone(toRaw(obj));
489
+ }
490
+ function useForm(options) {
491
+ const shouldRemember = options?.key !== false;
492
+ const historyKey = options?.key ?? "form:default";
493
+ const historyData = shouldRemember ? router.history.get(historyKey) : void 0;
494
+ const timeoutIds = {
495
+ recentlyFailed: void 0,
496
+ recentlySuccessful: void 0
497
+ };
498
+ const initial = readonly(safeClone(options.fields));
499
+ const loaded = readonly(safeClone(historyData?.fields ?? options.fields));
500
+ const fields = reactive(safeClone(historyData?.fields ?? options.fields));
501
+ const errors = ref(historyData?.errors ?? {});
502
+ const isDirty = ref(false);
503
+ const recentlySuccessful = ref(false);
504
+ const successful = ref(false);
505
+ const recentlyFailed = ref(false);
506
+ const failed = ref(false);
507
+ const processing = ref(false);
508
+ function reset(...keys) {
509
+ if (keys.length === 0) {
510
+ keys = Object.keys(fields);
511
+ }
512
+ keys.forEach((key) => Reflect.set(fields, key, safeClone(Reflect.get(initial, key))));
513
+ clearErrors();
514
+ }
515
+ function submit(optionsOverrides) {
516
+ const url = typeof options.url === "function" ? options.url() : options.url;
517
+ const data = typeof options.transform === "function" ? options.transform?.(fields) : fields;
518
+ return router.visit({
519
+ url: url ?? state.context.value?.url,
520
+ method: options.method ?? "POST",
521
+ ...optionsOverrides,
522
+ data: safeClone(data),
523
+ preserveState: optionsOverrides?.preserveState === void 0 && options.method !== "GET" ? true : optionsOverrides?.preserveState,
524
+ hooks: {
525
+ before: (visit) => {
526
+ failed.value = false;
527
+ successful.value = false;
528
+ recentlySuccessful.value = false;
529
+ clearTimeout(timeoutIds.recentlySuccessful);
530
+ clearTimeout(timeoutIds.recentlyFailed);
531
+ clearErrors();
532
+ return options.hooks?.before?.(visit);
533
+ },
534
+ start: (context) => {
535
+ processing.value = true;
536
+ return options.hooks?.start?.(context);
537
+ },
538
+ error: (incoming) => {
539
+ setErrors(incoming);
540
+ failed.value = true;
541
+ recentlyFailed.value = true;
542
+ timeoutIds.recentlyFailed = setTimeout(() => recentlyFailed.value = false, options?.timeout ?? 5e3);
543
+ return options.hooks?.error?.(incoming);
544
+ },
545
+ success: (payload) => {
546
+ if (options?.reset !== false) {
547
+ reset();
548
+ }
549
+ successful.value = true;
550
+ recentlySuccessful.value = true;
551
+ timeoutIds.recentlySuccessful = setTimeout(() => recentlySuccessful.value = false, options?.timeout ?? 5e3);
552
+ return options.hooks?.success?.(payload);
553
+ },
554
+ after: (context) => {
555
+ processing.value = false;
556
+ return options.hooks?.after?.(context);
557
+ }
558
+ }
559
+ });
560
+ }
561
+ function clearErrors() {
562
+ errors.value = {};
563
+ }
564
+ function setErrors(incoming) {
565
+ errors.value = incoming;
566
+ }
567
+ function abort() {
568
+ router.abort();
569
+ }
570
+ watch([fields, processing, errors], () => {
571
+ isDirty.value = !isEqual(toRaw(loaded), toRaw(fields));
572
+ if (shouldRemember) {
573
+ router.history.remember(historyKey, {
574
+ fields: toRaw(fields),
575
+ errors: toRaw(errors.value)
576
+ });
577
+ }
578
+ }, { deep: true, immediate: true });
579
+ return reactive({
580
+ reset,
581
+ initial,
582
+ fields,
583
+ loaded,
584
+ submit,
585
+ abort,
586
+ setErrors,
587
+ clearErrors,
588
+ hasErrors: computed(() => Object.values(errors.value).length > 0),
589
+ isDirty: readonly(isDirty),
590
+ errors: readonly(errors),
591
+ processing: readonly(processing),
592
+ successful: readonly(successful),
593
+ failed: readonly(failed),
594
+ recentlySuccessful: readonly(recentlySuccessful),
595
+ recentlyFailed: readonly(recentlyFailed)
596
+ });
597
+ }
598
+
599
+ function useHistoryState(key, initial) {
600
+ const value = ref(router.history.get(key) ?? initial);
601
+ watch(value, (value2) => {
602
+ router.history.remember(key, toRaw(value2));
603
+ }, { immediate: true, deep: true });
604
+ return value;
605
+ }
606
+
607
+ function useBackForward() {
608
+ const callbacks = [];
609
+ registerHook("navigate", (options) => {
610
+ if (options.isBackForward) {
611
+ callbacks.forEach((fn) => fn(state.context.value));
612
+ callbacks.splice(0, callbacks.length);
613
+ }
614
+ });
615
+ function onBackForward(fn) {
616
+ callbacks.push(fn);
617
+ }
618
+ function reloadOnBackForward(options) {
619
+ onBackForward(() => router.reload(options));
620
+ }
621
+ return {
622
+ onBackForward,
623
+ reloadOnBackForward
624
+ };
625
+ }
626
+
627
+ function usePaginator(paginator) {
628
+ const meta = paginator.meta ?? paginator;
629
+ const links = meta.links ?? paginator.links;
630
+ const items = links.map((link, index) => {
631
+ return {
632
+ url: link.url,
633
+ label: link.label,
634
+ isPage: !isNaN(+link.label),
635
+ isPrevious: index === 0,
636
+ isNext: index === links.length - 1,
637
+ isCurrent: link.active,
638
+ isSeparator: link.label === "...",
639
+ isActive: !!link.url && !link.active
640
+ };
641
+ });
642
+ const pages = items.filter((item) => item.isPage || item.isSeparator);
643
+ const current = items.find((item) => item.isCurrent);
644
+ const previous = items.find((item) => item.isPrevious);
645
+ const next = items.find((item) => item.isNext);
646
+ const first = { ...items[1], isActive: items[1].url !== current?.url, label: "&laquo;" };
647
+ const last = { ...items[items.length - 1], isActive: items[items.length - 1].url !== current?.url, label: "&raquo;" };
648
+ const from = meta.from;
649
+ const to = meta.to;
650
+ const total = meta.total;
651
+ return { pages, items, previous, next, first, last, total, from, to };
652
+ }
653
+
654
+ function useLayout(layout) {
655
+ state.setViewLayout(layout);
656
+ }
657
+
658
+ class Route {
659
+ constructor(name, absolute) {
660
+ this.name = name;
661
+ this.absolute = absolute;
662
+ this.definition = Route.getDefinition(name);
663
+ }
664
+ static getDefinition(name) {
665
+ if (!state.routes.value) {
666
+ throw new Error("Routing is not initialized. Have you enabled the Vite plugin?");
667
+ }
668
+ const routes = state.routes.value;
669
+ const route = routes?.routes?.[name];
670
+ if (!route) {
671
+ throw new Error(`Route ${name.toString()} does not exist.`);
672
+ }
673
+ return route;
674
+ }
675
+ get template() {
676
+ const origin = !this.absolute ? "" : this.definition.domain ? `${state.routes.value?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${state.routes.value?.port ? `:${state.routes.value?.port}` : ""}` : state.routes.value?.url;
677
+ return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
678
+ }
679
+ get parameterSegments() {
680
+ return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
681
+ name: segment.replace(/{|\??}/g, ""),
682
+ required: !/\?}$/.test(segment)
683
+ })) ?? [];
684
+ }
685
+ matchesUrl(url) {
686
+ if (!this.definition.methods.includes("GET")) {
687
+ return false;
688
+ }
689
+ const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
690
+ const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
691
+ return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
692
+ }).replace(/^\w+:\/\//, "");
693
+ const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
694
+ const matches = new RegExp(`^${pattern}/?$`).exec(location);
695
+ return matches ? { params: matches.groups, query: parse(query) } : false;
696
+ }
697
+ compile(params) {
698
+ const segments = this.parameterSegments;
699
+ if (!segments.length) {
700
+ return this.template;
701
+ }
702
+ return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
703
+ if (!optional && [null, void 0].includes(params?.[segment])) {
704
+ throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
705
+ }
706
+ if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
707
+ return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
708
+ }
709
+ if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
710
+ throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
711
+ }
712
+ return encodeURIComponent(params[segment] ?? "");
713
+ }).replace(/\/+$/, "");
714
+ }
715
+ }
716
+
717
+ class Router extends String {
718
+ constructor(name, parameters, absolute = true) {
719
+ super();
720
+ this.route = new Route(name, absolute);
721
+ this.setParameters(parameters);
722
+ }
723
+ toString() {
724
+ const unhandled = Object.keys(this.parameters).filter((key) => !this.route.parameterSegments.some(({ name }) => name === key)).filter((key) => key !== "_query").reduce((result, current) => ({ ...result, [current]: this.parameters[current] }), {});
725
+ return this.route.compile(this.parameters) + stringify({ ...unhandled, ...this.parameters._query }, {
726
+ addQueryPrefix: true,
727
+ arrayFormat: "indices",
728
+ encodeValuesOnly: true,
729
+ skipNulls: true,
730
+ encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
731
+ });
732
+ }
733
+ static has(name) {
734
+ try {
735
+ Route.getDefinition(name);
736
+ return true;
737
+ } catch {
738
+ return false;
739
+ }
740
+ }
741
+ setParameters(parameters) {
742
+ this.parameters = parameters ?? {};
743
+ this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
744
+ const segments = this.route.parameterSegments.filter(({ name }) => !state.routes.value?.defaults[name]);
745
+ if (Array.isArray(this.parameters)) {
746
+ this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
747
+ } else if (segments.length === 1 && !this.parameters[segments[0].name] && (Reflect.has(this.parameters, Object.values(this.route.definition.bindings)[0]) || Reflect.has(this.parameters, "id"))) {
748
+ this.parameters = { [segments[0].name]: this.parameters };
749
+ }
750
+ this.parameters = {
751
+ ...this.getDefaults(),
752
+ ...this.substituteBindings()
753
+ };
754
+ }
755
+ getDefaults() {
756
+ return this.route.parameterSegments.filter(({ name }) => state.routes.value?.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: state.routes.value?.defaults[name] }), {});
757
+ }
758
+ substituteBindings() {
759
+ return Object.entries(this.parameters).reduce((result, [key, value]) => {
760
+ if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
761
+ return { ...result, [key]: value };
762
+ }
763
+ if (!Reflect.has(value, this.route.definition.bindings[key])) {
764
+ if (Reflect.has(value, "id")) {
765
+ this.route.definition.bindings[key] = "id";
766
+ } else {
767
+ throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
768
+ }
769
+ }
770
+ return { ...result, [key]: value[this.route.definition.bindings[key]] };
771
+ }, {});
772
+ }
773
+ valueOf() {
774
+ return this.toString();
775
+ }
776
+ }
777
+
778
+ function route(name, parameters, absolute) {
779
+ return new Router(name, parameters, absolute).toString();
780
+ }
781
+
782
+ export { HybridlyImports, HybridlyResolver, RouterLink, initializeHybridly, route, useBackForward, useContext, useForm, useHistoryState, useLayout, usePaginator, useProperties, useProperty, useRouter };