@hybridly/core 0.0.1-alpha.13 → 0.0.1-alpha.15

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.cjs CHANGED
@@ -306,6 +306,139 @@ function createSerializer(options) {
306
306
  };
307
307
  }
308
308
 
309
+ class Route {
310
+ constructor(name, absolute) {
311
+ this.name = name;
312
+ this.absolute = absolute;
313
+ this.definition = Route.getDefinition(name);
314
+ }
315
+ static getDefinition(name) {
316
+ const context = getInternalRouterContext();
317
+ if (!context.routing) {
318
+ throw new Error("Routing is not initialized. Make sure the Vite plugin is enabled and that `virtual:hybridly/router` is imported and that `php artisan route:list` returns no error.");
319
+ }
320
+ const route = context.routing?.routes?.[name];
321
+ if (!route) {
322
+ throw new Error(`Route ${name.toString()} does not exist.`);
323
+ }
324
+ return route;
325
+ }
326
+ get template() {
327
+ const context = getInternalRouterContext();
328
+ const origin = !this.absolute ? "" : this.definition.domain ? `${context.routing?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${context.routing?.port ? `:${context.routing?.port}` : ""}` : context.routing?.url;
329
+ return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
330
+ }
331
+ get parameterSegments() {
332
+ return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
333
+ name: segment.replace(/{|\??}/g, ""),
334
+ required: !/\?}$/.test(segment)
335
+ })) ?? [];
336
+ }
337
+ matchesUrl(url) {
338
+ if (!this.definition.method.includes("GET")) {
339
+ return false;
340
+ }
341
+ const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
342
+ const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
343
+ return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
344
+ }).replace(/^\w+:\/\//, "");
345
+ const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
346
+ const matches = new RegExp(`^${pattern}/?$`).exec(location);
347
+ return matches ? { params: matches.groups, query: qs.parse(query) } : false;
348
+ }
349
+ compile(params) {
350
+ const segments = this.parameterSegments;
351
+ if (!segments.length) {
352
+ return this.template;
353
+ }
354
+ return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
355
+ if (!optional && [null, void 0].includes(params?.[segment])) {
356
+ throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
357
+ }
358
+ if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
359
+ return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
360
+ }
361
+ if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
362
+ throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
363
+ }
364
+ return encodeURIComponent(params[segment] ?? "");
365
+ }).replace(/\/+$/, "");
366
+ }
367
+ }
368
+
369
+ class Router extends String {
370
+ constructor(name, parameters, absolute = true) {
371
+ super();
372
+ const context = getInternalRouterContext();
373
+ this.route = new Route(name, absolute);
374
+ this.routing = context.routing;
375
+ this.setParameters(parameters);
376
+ }
377
+ toString() {
378
+ 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] }), {});
379
+ return this.route.compile(this.parameters) + qs.stringify({ ...unhandled, ...this.parameters._query }, {
380
+ addQueryPrefix: true,
381
+ arrayFormat: "indices",
382
+ encodeValuesOnly: true,
383
+ skipNulls: true,
384
+ encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
385
+ });
386
+ }
387
+ static has(name) {
388
+ try {
389
+ Route.getDefinition(name);
390
+ return true;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+ setParameters(parameters) {
396
+ this.parameters = parameters ?? {};
397
+ this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
398
+ const segments = this.route.parameterSegments.filter(({ name }) => !this.routing.defaults[name]);
399
+ if (Array.isArray(this.parameters)) {
400
+ this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
401
+ } 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"))) {
402
+ this.parameters = { [segments[0].name]: this.parameters };
403
+ }
404
+ this.parameters = {
405
+ ...this.getDefaults(),
406
+ ...this.substituteBindings()
407
+ };
408
+ }
409
+ getDefaults() {
410
+ return this.route.parameterSegments.filter(({ name }) => this.routing.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: this.routing.defaults[name] }), {});
411
+ }
412
+ substituteBindings() {
413
+ return Object.entries(this.parameters).reduce((result, [key, value]) => {
414
+ if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
415
+ return { ...result, [key]: value };
416
+ }
417
+ if (!Reflect.has(value, this.route.definition.bindings[key])) {
418
+ if (Reflect.has(value, "id")) {
419
+ this.route.definition.bindings[key] = "id";
420
+ } else {
421
+ throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
422
+ }
423
+ }
424
+ return { ...result, [key]: value[this.route.definition.bindings[key]] };
425
+ }, {});
426
+ }
427
+ valueOf() {
428
+ return this.toString();
429
+ }
430
+ }
431
+
432
+ function route(name, parameters, absolute) {
433
+ return new Router(name, parameters, absolute).toString();
434
+ }
435
+ function updateRoutingConfiguration(routing) {
436
+ if (!routing) {
437
+ return;
438
+ }
439
+ setContext({ routing });
440
+ }
441
+
309
442
  const state = {
310
443
  initialized: false,
311
444
  context: {}
@@ -325,10 +458,14 @@ async function initializeContext(options) {
325
458
  ...options.payload,
326
459
  serializer: createSerializer(options),
327
460
  url: makeUrl(options.payload.url).toString(),
328
- adapter: options.adapter,
461
+ adapter: {
462
+ ...options.adapter,
463
+ updateRoutingConfiguration
464
+ },
329
465
  scrollRegions: [],
330
466
  plugins: options.plugins ?? [],
331
467
  axios: options.axios ?? axios__default.create(),
468
+ routing: options.routing,
332
469
  hooks: {},
333
470
  state: {}
334
471
  };
@@ -409,6 +546,11 @@ const router = {
409
546
  delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
410
547
  local: async (url, options) => await performLocalNavigation(url, options),
411
548
  external: (url, data = {}) => navigateToExternalUrl(url, data),
549
+ to: async (name, parameters, options) => {
550
+ const url = new Router(name, parameters).toString();
551
+ const method = Route.getDefinition(name).method.at(0);
552
+ return await performHybridNavigation({ url, ...options, method });
553
+ },
412
554
  history: {
413
555
  get: (key) => getKeyFromHistory(key),
414
556
  remember: (key, value) => remember(key, value)
@@ -423,6 +565,11 @@ async function performHybridNavigation(options) {
423
565
  const context = getRouterContext();
424
566
  utils.debug.router("Making a hybrid navigation:", { context, options, navigationId });
425
567
  try {
568
+ if (!options.method) {
569
+ utils.debug.router("Setting method to GET because none was provided.");
570
+ options.method = "GET";
571
+ }
572
+ options.method = options.method.toUpperCase();
426
573
  if ((utils.hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
427
574
  options.data = utils.objectToFormData(options.data);
428
575
  utils.debug.router("Converted data to FormData.", options.data);
@@ -432,10 +579,6 @@ async function performHybridNavigation(options) {
432
579
  options.method = "POST";
433
580
  }
434
581
  }
435
- if (!options.method) {
436
- utils.debug.router("Setting method to GET because none was provided.");
437
- options.method = "GET";
438
- }
439
582
  if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
440
583
  utils.debug.router("Transforming data to query parameters.", options.data);
441
584
  options.url = makeUrl(options.url ?? context.url, {
@@ -551,12 +694,10 @@ async function performHybridNavigation(options) {
551
694
  await utils.match(error.constructor.name, {
552
695
  NavigationCancelledError: async () => {
553
696
  utils.debug.router('The request was cancelled through the "before" hook.', error);
554
- console.warn(error);
555
697
  await runHooks("abort", options.hooks, context);
556
698
  },
557
699
  AbortError: async () => {
558
700
  utils.debug.router("The request was cancelled.", error);
559
- console.warn(error);
560
701
  await runHooks("abort", options.hooks, context);
561
702
  },
562
703
  NotAHybridResponseError: async () => {
@@ -683,5 +824,6 @@ exports.definePlugin = definePlugin;
683
824
  exports.getRouterContext = getRouterContext;
684
825
  exports.makeUrl = makeUrl;
685
826
  exports.registerHook = registerHook;
827
+ exports.route = route;
686
828
  exports.router = router;
687
829
  exports.sameUrls = sameUrls;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RequestData } from '@hybridly/utils';
2
- import { AxiosResponse, Axios, AxiosProgressEvent } from 'axios';
2
+ import { AxiosResponse, AxiosProgressEvent, Axios } from 'axios';
3
3
 
4
4
  type MaybePromise<T> = T | Promise<T>;
5
5
 
@@ -72,72 +72,6 @@ interface Plugin extends Partial<Hooks> {
72
72
  }
73
73
  declare function definePlugin(plugin: Plugin): Plugin;
74
74
 
75
- /** Options for creating a router context. */
76
- interface RouterContextOptions {
77
- /** The initial payload served by the browser. */
78
- payload: HybridPayload;
79
- /** Adapter-specific functions. */
80
- adapter: Adapter;
81
- /** History state serializer. */
82
- serializer?: Serializer;
83
- /** List of plugins. */
84
- plugins?: Plugin[];
85
- /** The Axios instance. */
86
- axios?: Axios;
87
- }
88
- /** Router context. */
89
- interface InternalRouterContext {
90
- /** The current, normalized URL. */
91
- url: string;
92
- /** The current view. */
93
- view: View;
94
- /** The current, optional dialog. */
95
- dialog?: View;
96
- /** The current local asset version. */
97
- version: string;
98
- /** The current adapter's functions. */
99
- adapter: Adapter;
100
- /** Scroll positions of the current page's DOM elements. */
101
- scrollRegions: ScrollRegion[];
102
- /** Arbitrary state. */
103
- state: Record<string, any>;
104
- /** Currently pending navigation. */
105
- pendingNavigation?: PendingNavigation;
106
- /** History state serializer. */
107
- serializer: Serializer;
108
- /** List of plugins. */
109
- plugins: Plugin[];
110
- /** Global hooks. */
111
- hooks: Partial<Record<keyof Hooks, Array<Function>>>;
112
- /** The Axios instance. */
113
- axios: Axios;
114
- }
115
- /** Router context. */
116
- type RouterContext = Readonly<InternalRouterContext>;
117
- /** Adapter-specific functions. */
118
- interface Adapter {
119
- /** Resolves a component from the given name. */
120
- resolveComponent: ResolveComponent;
121
- /** Swaps to the given view. */
122
- swapView: SwapView;
123
- /** Swaps to the given dialog. */
124
- swapDialog: SwapDialog;
125
- /** Called when the context is updated. */
126
- update?: (context: InternalRouterContext) => void;
127
- }
128
- interface ScrollRegion {
129
- top: number;
130
- left: number;
131
- }
132
- /** Provides methods to serialize the state into the history state. */
133
- interface Serializer {
134
- serialize: <T>(view: T) => any;
135
- unserialize: <T>(state: any) => T;
136
- }
137
-
138
- /** Gets the current context. */
139
- declare function getRouterContext(): RouterContext;
140
-
141
75
  type UrlResolvable = string | URL | Location;
142
76
  type UrlTransformable = Partial<Omit<URL, 'searchParams' | 'toJSON' | 'toString'>> & {
143
77
  query?: any;
@@ -207,7 +141,7 @@ interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
207
141
  /** The URL to navigation. */
208
142
  url?: UrlResolvable;
209
143
  /** HTTP verb to use for the request. */
210
- method?: Method;
144
+ method?: Method | Lowercase<Method>;
211
145
  /** Body of the request. */
212
146
  data?: RequestData;
213
147
  /** Which properties to update for this navigation. Other properties will be ignored. */
@@ -248,6 +182,8 @@ interface Router {
248
182
  navigate: (options: HybridRequestOptions) => Promise<NavigationResponse>;
249
183
  /** Reloads the current page. */
250
184
  reload: (options?: HybridRequestOptions) => Promise<NavigationResponse>;
185
+ /** Makes a request to given named route. The HTTP verb is determined automatically but can be overriden. */
186
+ to: <T extends RouteName>(name: T, parameters?: RouteParameters<T>, options?: Omit<HybridRequestOptions, 'url'>) => Promise<NavigationResponse>;
251
187
  /** Makes a GET request to the given URL. */
252
188
  get: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
253
189
  /** Makes a POST request to the given URL. */
@@ -324,6 +260,97 @@ interface Errors {
324
260
  [key: string]: string;
325
261
  }
326
262
 
263
+ interface RoutingConfiguration {
264
+ url: string;
265
+ port?: number;
266
+ defaults: Record<string, any>;
267
+ routes: Record<string, RouteDefinition>;
268
+ }
269
+ interface RouteDefinition {
270
+ uri: string;
271
+ method: Method[];
272
+ bindings: Record<string, string>;
273
+ domain?: string;
274
+ wheres?: Record<string, string>;
275
+ }
276
+ interface GlobalRouteCollection extends RoutingConfiguration {
277
+ }
278
+ type RouteName = keyof GlobalRouteCollection['routes'];
279
+ type RouteParameters<T extends RouteName> = Record<keyof GlobalRouteCollection['routes'][T]['bindings'], any> & Record<string, any>;
280
+
281
+ /** Options for creating a router context. */
282
+ interface RouterContextOptions {
283
+ /** The initial payload served by the browser. */
284
+ payload: HybridPayload;
285
+ /** Adapter-specific functions. */
286
+ adapter: Adapter;
287
+ /** History state serializer. */
288
+ serializer?: Serializer;
289
+ /** List of plugins. */
290
+ plugins?: Plugin[];
291
+ /** The Axios instance. */
292
+ axios?: Axios;
293
+ /** Initial routing configuration. */
294
+ routing?: RoutingConfiguration;
295
+ }
296
+ /** Router context. */
297
+ interface InternalRouterContext {
298
+ /** The current, normalized URL. */
299
+ url: string;
300
+ /** The current view. */
301
+ view: View;
302
+ /** The current, optional dialog. */
303
+ dialog?: View;
304
+ /** The current local asset version. */
305
+ version: string;
306
+ /** The current adapter's functions. */
307
+ adapter: ResolvedAdapter;
308
+ /** Scroll positions of the current page's DOM elements. */
309
+ scrollRegions: ScrollRegion[];
310
+ /** Arbitrary state. */
311
+ state: Record<string, any>;
312
+ /** Currently pending navigation. */
313
+ pendingNavigation?: PendingNavigation;
314
+ /** History state serializer. */
315
+ serializer: Serializer;
316
+ /** List of plugins. */
317
+ plugins: Plugin[];
318
+ /** Global hooks. */
319
+ hooks: Partial<Record<keyof Hooks, Array<Function>>>;
320
+ /** The Axios instance. */
321
+ axios: Axios;
322
+ /** Routing configuration. */
323
+ routing?: RoutingConfiguration;
324
+ }
325
+ /** Router context. */
326
+ type RouterContext = Readonly<InternalRouterContext>;
327
+ /** Adapter-specific functions. */
328
+ interface Adapter {
329
+ /** Resolves a component from the given name. */
330
+ resolveComponent: ResolveComponent;
331
+ /** Swaps to the given view. */
332
+ swapView: SwapView;
333
+ /** Swaps to the given dialog. */
334
+ swapDialog: SwapDialog;
335
+ /** Called when the context is updated. */
336
+ update?: (context: InternalRouterContext) => void;
337
+ }
338
+ interface ResolvedAdapter extends Adapter {
339
+ updateRoutingConfiguration: (routing?: RoutingConfiguration) => void;
340
+ }
341
+ interface ScrollRegion {
342
+ top: number;
343
+ left: number;
344
+ }
345
+ /** Provides methods to serialize the state into the history state. */
346
+ interface Serializer {
347
+ serialize: <T>(view: T) => any;
348
+ unserialize: <T>(state: any) => T;
349
+ }
350
+
351
+ /** Gets the current context. */
352
+ declare function getRouterContext(): RouterContext;
353
+
327
354
  /**
328
355
  * The hybridly router.
329
356
  * This is the core function that you can use to navigate in
@@ -346,6 +373,11 @@ interface Authorizable<Authorizations extends Record<string, boolean>> {
346
373
  */
347
374
  declare function can<Authorizations extends Record<string, boolean>, Data extends Authorizable<Authorizations>, Action extends keyof Data['authorization']>(resource: Data, action: Action): Authorizations[Action];
348
375
 
376
+ /**
377
+ * Generates a route from the given route name.
378
+ */
379
+ declare function route<T extends RouteName>(name: T, parameters?: RouteParameters<T>, absolute?: boolean): string;
380
+
349
381
  declare const STORAGE_EXTERNAL_KEY = "hybridly:external";
350
382
  declare const HYBRIDLY_HEADER = "x-hybrid";
351
383
  declare const EXTERNAL_NAVIGATION_HEADER: string;
@@ -382,4 +414,4 @@ declare namespace constants {
382
414
  };
383
415
  }
384
416
 
385
- export { Authorizable, HybridPayload, HybridRequestOptions, MaybePromise, Method, NavigationResponse, Plugin, Progress, ResolveComponent, Router, RouterContext, RouterContextOptions, UrlResolvable, can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, router, sameUrls };
417
+ export { Authorizable, GlobalRouteCollection, HybridPayload, HybridRequestOptions, MaybePromise, Method, NavigationResponse, Plugin, Progress, ResolveComponent, RouteDefinition, RouteName, RouteParameters, Router, RouterContext, RouterContextOptions, RoutingConfiguration, UrlResolvable, can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import qs from 'qs';
1
+ import qs, { parse, stringify } from 'qs';
2
2
  import { debug, merge, debounce, random, hasFiles, objectToFormData, when, match, showResponseErrorModal } from '@hybridly/utils';
3
3
  import axios from 'axios';
4
4
 
@@ -297,6 +297,139 @@ function createSerializer(options) {
297
297
  };
298
298
  }
299
299
 
300
+ class Route {
301
+ constructor(name, absolute) {
302
+ this.name = name;
303
+ this.absolute = absolute;
304
+ this.definition = Route.getDefinition(name);
305
+ }
306
+ static getDefinition(name) {
307
+ const context = getInternalRouterContext();
308
+ if (!context.routing) {
309
+ throw new Error("Routing is not initialized. Make sure the Vite plugin is enabled and that `virtual:hybridly/router` is imported and that `php artisan route:list` returns no error.");
310
+ }
311
+ const route = context.routing?.routes?.[name];
312
+ if (!route) {
313
+ throw new Error(`Route ${name.toString()} does not exist.`);
314
+ }
315
+ return route;
316
+ }
317
+ get template() {
318
+ const context = getInternalRouterContext();
319
+ const origin = !this.absolute ? "" : this.definition.domain ? `${context.routing?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${context.routing?.port ? `:${context.routing?.port}` : ""}` : context.routing?.url;
320
+ return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
321
+ }
322
+ get parameterSegments() {
323
+ return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
324
+ name: segment.replace(/{|\??}/g, ""),
325
+ required: !/\?}$/.test(segment)
326
+ })) ?? [];
327
+ }
328
+ matchesUrl(url) {
329
+ if (!this.definition.method.includes("GET")) {
330
+ return false;
331
+ }
332
+ const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
333
+ const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
334
+ return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
335
+ }).replace(/^\w+:\/\//, "");
336
+ const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
337
+ const matches = new RegExp(`^${pattern}/?$`).exec(location);
338
+ return matches ? { params: matches.groups, query: parse(query) } : false;
339
+ }
340
+ compile(params) {
341
+ const segments = this.parameterSegments;
342
+ if (!segments.length) {
343
+ return this.template;
344
+ }
345
+ return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
346
+ if (!optional && [null, void 0].includes(params?.[segment])) {
347
+ throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
348
+ }
349
+ if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
350
+ return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
351
+ }
352
+ if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
353
+ throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
354
+ }
355
+ return encodeURIComponent(params[segment] ?? "");
356
+ }).replace(/\/+$/, "");
357
+ }
358
+ }
359
+
360
+ class Router extends String {
361
+ constructor(name, parameters, absolute = true) {
362
+ super();
363
+ const context = getInternalRouterContext();
364
+ this.route = new Route(name, absolute);
365
+ this.routing = context.routing;
366
+ this.setParameters(parameters);
367
+ }
368
+ toString() {
369
+ 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] }), {});
370
+ return this.route.compile(this.parameters) + stringify({ ...unhandled, ...this.parameters._query }, {
371
+ addQueryPrefix: true,
372
+ arrayFormat: "indices",
373
+ encodeValuesOnly: true,
374
+ skipNulls: true,
375
+ encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
376
+ });
377
+ }
378
+ static has(name) {
379
+ try {
380
+ Route.getDefinition(name);
381
+ return true;
382
+ } catch {
383
+ return false;
384
+ }
385
+ }
386
+ setParameters(parameters) {
387
+ this.parameters = parameters ?? {};
388
+ this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
389
+ const segments = this.route.parameterSegments.filter(({ name }) => !this.routing.defaults[name]);
390
+ if (Array.isArray(this.parameters)) {
391
+ this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
392
+ } 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"))) {
393
+ this.parameters = { [segments[0].name]: this.parameters };
394
+ }
395
+ this.parameters = {
396
+ ...this.getDefaults(),
397
+ ...this.substituteBindings()
398
+ };
399
+ }
400
+ getDefaults() {
401
+ return this.route.parameterSegments.filter(({ name }) => this.routing.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: this.routing.defaults[name] }), {});
402
+ }
403
+ substituteBindings() {
404
+ return Object.entries(this.parameters).reduce((result, [key, value]) => {
405
+ if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
406
+ return { ...result, [key]: value };
407
+ }
408
+ if (!Reflect.has(value, this.route.definition.bindings[key])) {
409
+ if (Reflect.has(value, "id")) {
410
+ this.route.definition.bindings[key] = "id";
411
+ } else {
412
+ throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
413
+ }
414
+ }
415
+ return { ...result, [key]: value[this.route.definition.bindings[key]] };
416
+ }, {});
417
+ }
418
+ valueOf() {
419
+ return this.toString();
420
+ }
421
+ }
422
+
423
+ function route(name, parameters, absolute) {
424
+ return new Router(name, parameters, absolute).toString();
425
+ }
426
+ function updateRoutingConfiguration(routing) {
427
+ if (!routing) {
428
+ return;
429
+ }
430
+ setContext({ routing });
431
+ }
432
+
300
433
  const state = {
301
434
  initialized: false,
302
435
  context: {}
@@ -316,10 +449,14 @@ async function initializeContext(options) {
316
449
  ...options.payload,
317
450
  serializer: createSerializer(options),
318
451
  url: makeUrl(options.payload.url).toString(),
319
- adapter: options.adapter,
452
+ adapter: {
453
+ ...options.adapter,
454
+ updateRoutingConfiguration
455
+ },
320
456
  scrollRegions: [],
321
457
  plugins: options.plugins ?? [],
322
458
  axios: options.axios ?? axios.create(),
459
+ routing: options.routing,
323
460
  hooks: {},
324
461
  state: {}
325
462
  };
@@ -400,6 +537,11 @@ const router = {
400
537
  delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
401
538
  local: async (url, options) => await performLocalNavigation(url, options),
402
539
  external: (url, data = {}) => navigateToExternalUrl(url, data),
540
+ to: async (name, parameters, options) => {
541
+ const url = new Router(name, parameters).toString();
542
+ const method = Route.getDefinition(name).method.at(0);
543
+ return await performHybridNavigation({ url, ...options, method });
544
+ },
403
545
  history: {
404
546
  get: (key) => getKeyFromHistory(key),
405
547
  remember: (key, value) => remember(key, value)
@@ -414,6 +556,11 @@ async function performHybridNavigation(options) {
414
556
  const context = getRouterContext();
415
557
  debug.router("Making a hybrid navigation:", { context, options, navigationId });
416
558
  try {
559
+ if (!options.method) {
560
+ debug.router("Setting method to GET because none was provided.");
561
+ options.method = "GET";
562
+ }
563
+ options.method = options.method.toUpperCase();
417
564
  if ((hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
418
565
  options.data = objectToFormData(options.data);
419
566
  debug.router("Converted data to FormData.", options.data);
@@ -423,10 +570,6 @@ async function performHybridNavigation(options) {
423
570
  options.method = "POST";
424
571
  }
425
572
  }
426
- if (!options.method) {
427
- debug.router("Setting method to GET because none was provided.");
428
- options.method = "GET";
429
- }
430
573
  if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
431
574
  debug.router("Transforming data to query parameters.", options.data);
432
575
  options.url = makeUrl(options.url ?? context.url, {
@@ -542,12 +685,10 @@ async function performHybridNavigation(options) {
542
685
  await match(error.constructor.name, {
543
686
  NavigationCancelledError: async () => {
544
687
  debug.router('The request was cancelled through the "before" hook.', error);
545
- console.warn(error);
546
688
  await runHooks("abort", options.hooks, context);
547
689
  },
548
690
  AbortError: async () => {
549
691
  debug.router("The request was cancelled.", error);
550
- console.warn(error);
551
692
  await runHooks("abort", options.hooks, context);
552
693
  },
553
694
  NotAHybridResponseError: async () => {
@@ -667,4 +808,4 @@ function can(resource, action) {
667
808
  return resource.authorization?.[action] ?? false;
668
809
  }
669
810
 
670
- export { can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, router, sameUrls };
811
+ export { can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridly/core",
3
- "version": "0.0.1-alpha.13",
3
+ "version": "0.0.1-alpha.15",
4
4
  "description": "A solution to develop server-driven, client-rendered applications",
5
5
  "keywords": [
6
6
  "hybridly",
@@ -36,8 +36,8 @@
36
36
  "axios": "^1"
37
37
  },
38
38
  "dependencies": {
39
- "@hybridly/utils": "0.0.1-alpha.13",
40
- "qs": "^6.11.0"
39
+ "qs": "^6.11.0",
40
+ "@hybridly/utils": "0.0.1-alpha.15"
41
41
  },
42
42
  "devDependencies": {
43
43
  "defu": "^6.1.1"