@hybridly/core 0.0.1-alpha.19 → 0.0.1-alpha.20

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
@@ -44,6 +44,21 @@ class NotAHybridResponseError extends Error {
44
44
  }
45
45
  class NavigationCancelledError extends Error {
46
46
  }
47
+ class RoutingNotInitialized extends Error {
48
+ constructor() {
49
+ super("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.");
50
+ }
51
+ }
52
+ class RouteNotFound extends Error {
53
+ constructor(name) {
54
+ super(`Route [${name}] does not exist.`);
55
+ }
56
+ }
57
+ class MissingRouteParameter extends Error {
58
+ constructor(parameter, routeName) {
59
+ super(`Parameter [${parameter}] is required for route [${routeName}].`);
60
+ }
61
+ }
47
62
 
48
63
  function definePlugin(plugin) {
49
64
  return plugin;
@@ -163,14 +178,15 @@ async function restoreScrollPositions() {
163
178
  }
164
179
  }
165
180
 
166
- function normalizeUrl(href) {
167
- return makeUrl(href).toString();
181
+ function normalizeUrl(href, trailingSlash) {
182
+ return makeUrl(href, { trailingSlash }).toString();
168
183
  }
169
184
  function makeUrl(href, transformations = {}) {
170
185
  try {
171
186
  const base = document?.location?.href === "//" ? void 0 : document.location.href;
172
187
  const url = new URL(String(href), base);
173
- Object.entries(transformations ?? {}).forEach(([key, value]) => {
188
+ transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
189
+ Object.entries(transformations).forEach(([key, value]) => {
174
190
  if (key === "query") {
175
191
  key = "search";
176
192
  value = qs__default.stringify(utils.merge(qs__default.parse(url.search, { ignoreQueryPrefix: true }), value), {
@@ -180,6 +196,10 @@ function makeUrl(href, transformations = {}) {
180
196
  }
181
197
  Reflect.set(url, key, value);
182
198
  });
199
+ if (transformations.trailingSlash === false) {
200
+ const _url = utils.removeTrailingSlash(url.toString().replace(/\/\?/, "?"));
201
+ url.toString = () => _url;
202
+ }
183
203
  return url;
184
204
  } catch (error) {
185
205
  throw new TypeError(`${href} is not resolvable to a valid URL.`);
@@ -311,131 +331,107 @@ function createSerializer(options) {
311
331
  };
312
332
  }
313
333
 
314
- class Route {
315
- constructor(name, absolute) {
316
- this.name = name;
317
- this.absolute = absolute;
318
- this.definition = Route.getDefinition(name);
319
- }
320
- static getDefinition(name) {
321
- const context = getInternalRouterContext();
322
- if (!context.routing) {
323
- 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.");
334
+ function generateRouteFromName(name, parameters, absolute, shouldThrow) {
335
+ const url = getUrlFromName(name, parameters, shouldThrow);
336
+ return absolute === false ? url.toString().replace(url.origin, "") : url.toString();
337
+ }
338
+ function getUrlFromName(name, parameters, shouldThrow) {
339
+ const routing = getRouting();
340
+ const definition = getRouteDefinition(name);
341
+ const transforms = getRouteTransformable(name, parameters, shouldThrow);
342
+ const url = makeUrl(routing.url, (url2) => ({
343
+ hostname: definition.domain || url2.hostname,
344
+ port: routing.port?.toString() || url2.port,
345
+ trailingSlash: false,
346
+ ...transforms
347
+ }));
348
+ return url;
349
+ }
350
+ function getRouteTransformable(routeName, routeParameters, shouldThrow) {
351
+ const routing = getRouting();
352
+ const definition = getRouteDefinition(routeName);
353
+ const parameters = routeParameters || {};
354
+ const missing = Object.keys(parameters);
355
+ const path = definition.uri.replace(/{([^}?]+)\??}/g, (match, parameterName) => {
356
+ const optional = /\?}$/.test(match);
357
+ const value = (() => {
358
+ const value2 = parameters[parameterName];
359
+ const bindingProperty = definition.bindings?.[parameterName];
360
+ if (bindingProperty && typeof value2 === "object") {
361
+ return value2[bindingProperty];
362
+ }
363
+ return value2;
364
+ })();
365
+ missing.splice(missing.indexOf(parameterName), 1);
366
+ if (value) {
367
+ const where = definition.wheres?.[parameterName];
368
+ if (where && !new RegExp(where).test(value)) {
369
+ console.warn(`[hybridly:routing] Parameter [${parameterName}] does not match the required format [${where}] for route [${routeName}].`);
370
+ }
371
+ return value;
324
372
  }
325
- const route = context.routing?.routes?.[name];
326
- if (!route) {
327
- throw new Error(`Route ${name.toString()} does not exist.`);
373
+ if (routing.defaults?.[parameterName]) {
374
+ return routing.defaults?.[parameterName];
328
375
  }
329
- return route;
330
- }
331
- get template() {
332
- const context = getInternalRouterContext();
333
- 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;
334
- return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
335
- }
336
- get parameterSegments() {
337
- return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
338
- name: segment.replace(/{|\??}/g, ""),
339
- required: !/\?}$/.test(segment)
340
- })) ?? [];
341
- }
342
- matchesUrl(url) {
343
- if (!this.definition.method.includes("GET")) {
344
- return false;
376
+ if (optional) {
377
+ return "";
345
378
  }
346
- const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
347
- const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
348
- return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
349
- }).replace(/^\w+:\/\//, "");
350
- const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
351
- const matches = new RegExp(`^${pattern}/?$`).exec(location);
352
- return matches ? { params: matches.groups, query: qs.parse(query) } : false;
353
- }
354
- compile(params) {
355
- const segments = this.parameterSegments;
356
- if (!segments.length) {
357
- return this.template;
379
+ if (shouldThrow === false) {
380
+ return "";
358
381
  }
359
- return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
360
- if (!optional && [null, void 0].includes(params?.[segment])) {
361
- throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
362
- }
363
- if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
364
- return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
365
- }
366
- if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
367
- throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
368
- }
369
- return encodeURIComponent(params[segment] ?? "");
370
- }).replace(/\/+$/, "");
382
+ throw new MissingRouteParameter(parameterName, routeName);
383
+ });
384
+ const remaining = Object.keys(parameters).filter((key) => missing.includes(key)).reduce((obj, key) => ({
385
+ ...obj,
386
+ [key]: parameters[key]
387
+ }), {});
388
+ return {
389
+ pathname: path,
390
+ search: qs__default.stringify(remaining, {
391
+ encodeValuesOnly: true,
392
+ arrayFormat: "indices",
393
+ addQueryPrefix: true
394
+ })
395
+ };
396
+ }
397
+ function getRouteDefinition(name) {
398
+ const routing = getRouting();
399
+ const definition = routing.routes[name];
400
+ if (!definition) {
401
+ throw new RouteNotFound(name);
371
402
  }
403
+ return definition;
372
404
  }
373
-
374
- class Router extends String {
375
- constructor(name, parameters, absolute = true) {
376
- super();
377
- const context = getInternalRouterContext();
378
- this.route = new Route(name, absolute);
379
- this.routing = context.routing;
380
- this.setParameters(parameters);
381
- }
382
- toString() {
383
- 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] }), {});
384
- return this.route.compile(this.parameters) + qs.stringify({ ...unhandled, ...this.parameters._query }, {
385
- addQueryPrefix: true,
386
- arrayFormat: "indices",
387
- encodeValuesOnly: true,
388
- skipNulls: true,
389
- encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
390
- });
405
+ function getRouting() {
406
+ const { routing } = getInternalRouterContext();
407
+ if (!routing) {
408
+ throw new RoutingNotInitialized();
391
409
  }
392
- static has(name) {
410
+ return routing;
411
+ }
412
+
413
+ function isCurrentFromName(name, parameters, mode = "loose") {
414
+ const location = window.location;
415
+ const matchee = (() => {
393
416
  try {
394
- Route.getDefinition(name);
395
- return true;
396
- } catch {
397
- return false;
417
+ return makeUrl(generateRouteFromName(name, parameters, true, false));
418
+ } catch (error) {
398
419
  }
420
+ })();
421
+ if (!matchee) {
422
+ return false;
399
423
  }
400
- setParameters(parameters) {
401
- this.parameters = parameters ?? {};
402
- this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
403
- const segments = this.route.parameterSegments.filter(({ name }) => !this.routing.defaults[name]);
404
- if (Array.isArray(this.parameters)) {
405
- this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
406
- } 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"))) {
407
- this.parameters = { [segments[0].name]: this.parameters };
408
- }
409
- this.parameters = {
410
- ...this.getDefaults(),
411
- ...this.substituteBindings()
412
- };
413
- }
414
- getDefaults() {
415
- return this.route.parameterSegments.filter(({ name }) => this.routing.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: this.routing.defaults[name] }), {});
416
- }
417
- substituteBindings() {
418
- return Object.entries(this.parameters).reduce((result, [key, value]) => {
419
- if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
420
- return { ...result, [key]: value };
421
- }
422
- if (!Reflect.has(value, this.route.definition.bindings[key])) {
423
- if (Reflect.has(value, "id")) {
424
- this.route.definition.bindings[key] = "id";
425
- } else {
426
- throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
427
- }
428
- }
429
- return { ...result, [key]: value[this.route.definition.bindings[key]] };
430
- }, {});
431
- }
432
- valueOf() {
433
- return this.toString();
424
+ if (mode === "strict") {
425
+ return location.href === matchee.href;
434
426
  }
427
+ return location.href.startsWith(matchee.href);
435
428
  }
436
429
 
437
430
  function route(name, parameters, absolute) {
438
- return new Router(name, parameters, absolute).toString();
431
+ return generateRouteFromName(name, parameters, absolute);
432
+ }
433
+ function current(name, parameters, mode = "loose") {
434
+ return isCurrentFromName(name, parameters, mode);
439
435
  }
440
436
  function updateRoutingConfiguration(routing) {
441
437
  if (!routing) {
@@ -549,8 +545,8 @@ const router = {
549
545
  local: async (url, options) => await performLocalNavigation(url, options),
550
546
  external: (url, data = {}) => navigateToExternalUrl(url, data),
551
547
  to: async (name, parameters, options) => {
552
- const url = new Router(name, parameters).toString();
553
- const method = Route.getDefinition(name).method.at(0);
548
+ const url = generateRouteFromName(name, parameters);
549
+ const method = getRouteDefinition(name).method.at(0);
554
550
  return await performHybridNavigation({ url, ...options, method });
555
551
  },
556
552
  history: {
@@ -598,8 +594,7 @@ async function performHybridNavigation(options) {
598
594
  }
599
595
  saveScrollPositions();
600
596
  if (options.url && options.transformUrl) {
601
- const transformUrl = typeof options.transformUrl === "function" ? options.transformUrl(makeUrl(options.url).toString()) : options.transformUrl;
602
- options.url = makeUrl(options.url, transformUrl);
597
+ options.url = makeUrl(options.url, options.transformUrl);
603
598
  }
604
599
  const targetUrl = makeUrl(options.url ?? context.url);
605
600
  const abortController = new AbortController();
@@ -827,6 +822,7 @@ function can(resource, action) {
827
822
  exports.can = can;
828
823
  exports.constants = constants;
829
824
  exports.createRouter = createRouter;
825
+ exports.current = current;
830
826
  exports.definePlugin = definePlugin;
831
827
  exports.getRouterContext = getRouterContext;
832
828
  exports.makeUrl = makeUrl;
package/dist/index.d.ts CHANGED
@@ -87,8 +87,10 @@ interface Plugin extends Partial<Hooks> {
87
87
  declare function definePlugin(plugin: Plugin): Plugin;
88
88
 
89
89
  type UrlResolvable = string | URL | Location;
90
- type UrlTransformable = Partial<Omit<URL, 'searchParams' | 'toJSON' | 'toString'>> & {
90
+ type UrlTransformable = BaseUrlTransformable | ((string: URL) => BaseUrlTransformable);
91
+ type BaseUrlTransformable = Partial<Omit<URL, 'searchParams' | 'toJSON' | 'toString'>> & {
91
92
  query?: any;
93
+ trailingSlash?: boolean;
92
94
  };
93
95
  /**
94
96
  * Converts an input to an URL, optionally changing its properties after initialization.
@@ -138,7 +140,7 @@ interface NavigationOptions {
138
140
  * }
139
141
  * ```
140
142
  */
141
- transformUrl?: UrlTransformable | ((url: string) => UrlTransformable);
143
+ transformUrl?: UrlTransformable;
142
144
  /**
143
145
  * Defines whether the history state should be updated.
144
146
  * @internal This is an advanced property meant to be used internally.
@@ -288,6 +290,7 @@ interface RouteDefinition {
288
290
  bindings: Record<string, string>;
289
291
  domain?: string;
290
292
  wheres?: Record<string, string>;
293
+ name: string;
291
294
  }
292
295
  interface GlobalRouteCollection extends RoutingConfiguration {
293
296
  }
@@ -393,6 +396,10 @@ declare function can<Authorizations extends Record<string, boolean>, Data extend
393
396
  * Generates a route from the given route name.
394
397
  */
395
398
  declare function route<T extends RouteName>(name: T, parameters?: RouteParameters<T>, absolute?: boolean): string;
399
+ /**
400
+ * Determines if the current route correspond to the given route name and parameters.
401
+ */
402
+ declare function current<T extends RouteName>(name: T, parameters?: RouteParameters<T>, mode?: 'loose' | 'strict'): boolean;
396
403
 
397
404
  declare const STORAGE_EXTERNAL_KEY = "hybridly:external";
398
405
  declare const HYBRIDLY_HEADER = "x-hybrid";
@@ -430,4 +437,4 @@ declare namespace constants {
430
437
  };
431
438
  }
432
439
 
433
- 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 };
440
+ export { Authorizable, GlobalRouteCollection, HybridPayload, HybridRequestOptions, MaybePromise, Method, NavigationResponse, Plugin, Progress, ResolveComponent, RouteDefinition, RouteName, RouteParameters, Router, RouterContext, RouterContextOptions, RoutingConfiguration, UrlResolvable, can, constants, createRouter, current, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { debug, merge, debounce, random, hasFiles, objectToFormData, when, match, showResponseErrorModal } from '@hybridly/utils';
1
+ import { debug, merge, removeTrailingSlash, debounce, random, hasFiles, objectToFormData, when, match, showResponseErrorModal } from '@hybridly/utils';
2
2
  import axios from 'axios';
3
- import qs, { parse, stringify } from 'qs';
3
+ import qs from 'qs';
4
4
 
5
5
  const STORAGE_EXTERNAL_KEY = "hybridly:external";
6
6
  const HYBRIDLY_HEADER = "x-hybrid";
@@ -35,6 +35,21 @@ class NotAHybridResponseError extends Error {
35
35
  }
36
36
  class NavigationCancelledError extends Error {
37
37
  }
38
+ class RoutingNotInitialized extends Error {
39
+ constructor() {
40
+ super("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.");
41
+ }
42
+ }
43
+ class RouteNotFound extends Error {
44
+ constructor(name) {
45
+ super(`Route [${name}] does not exist.`);
46
+ }
47
+ }
48
+ class MissingRouteParameter extends Error {
49
+ constructor(parameter, routeName) {
50
+ super(`Parameter [${parameter}] is required for route [${routeName}].`);
51
+ }
52
+ }
38
53
 
39
54
  function definePlugin(plugin) {
40
55
  return plugin;
@@ -154,14 +169,15 @@ async function restoreScrollPositions() {
154
169
  }
155
170
  }
156
171
 
157
- function normalizeUrl(href) {
158
- return makeUrl(href).toString();
172
+ function normalizeUrl(href, trailingSlash) {
173
+ return makeUrl(href, { trailingSlash }).toString();
159
174
  }
160
175
  function makeUrl(href, transformations = {}) {
161
176
  try {
162
177
  const base = document?.location?.href === "//" ? void 0 : document.location.href;
163
178
  const url = new URL(String(href), base);
164
- Object.entries(transformations ?? {}).forEach(([key, value]) => {
179
+ transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
180
+ Object.entries(transformations).forEach(([key, value]) => {
165
181
  if (key === "query") {
166
182
  key = "search";
167
183
  value = qs.stringify(merge(qs.parse(url.search, { ignoreQueryPrefix: true }), value), {
@@ -171,6 +187,10 @@ function makeUrl(href, transformations = {}) {
171
187
  }
172
188
  Reflect.set(url, key, value);
173
189
  });
190
+ if (transformations.trailingSlash === false) {
191
+ const _url = removeTrailingSlash(url.toString().replace(/\/\?/, "?"));
192
+ url.toString = () => _url;
193
+ }
174
194
  return url;
175
195
  } catch (error) {
176
196
  throw new TypeError(`${href} is not resolvable to a valid URL.`);
@@ -302,131 +322,107 @@ function createSerializer(options) {
302
322
  };
303
323
  }
304
324
 
305
- class Route {
306
- constructor(name, absolute) {
307
- this.name = name;
308
- this.absolute = absolute;
309
- this.definition = Route.getDefinition(name);
310
- }
311
- static getDefinition(name) {
312
- const context = getInternalRouterContext();
313
- if (!context.routing) {
314
- 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.");
325
+ function generateRouteFromName(name, parameters, absolute, shouldThrow) {
326
+ const url = getUrlFromName(name, parameters, shouldThrow);
327
+ return absolute === false ? url.toString().replace(url.origin, "") : url.toString();
328
+ }
329
+ function getUrlFromName(name, parameters, shouldThrow) {
330
+ const routing = getRouting();
331
+ const definition = getRouteDefinition(name);
332
+ const transforms = getRouteTransformable(name, parameters, shouldThrow);
333
+ const url = makeUrl(routing.url, (url2) => ({
334
+ hostname: definition.domain || url2.hostname,
335
+ port: routing.port?.toString() || url2.port,
336
+ trailingSlash: false,
337
+ ...transforms
338
+ }));
339
+ return url;
340
+ }
341
+ function getRouteTransformable(routeName, routeParameters, shouldThrow) {
342
+ const routing = getRouting();
343
+ const definition = getRouteDefinition(routeName);
344
+ const parameters = routeParameters || {};
345
+ const missing = Object.keys(parameters);
346
+ const path = definition.uri.replace(/{([^}?]+)\??}/g, (match, parameterName) => {
347
+ const optional = /\?}$/.test(match);
348
+ const value = (() => {
349
+ const value2 = parameters[parameterName];
350
+ const bindingProperty = definition.bindings?.[parameterName];
351
+ if (bindingProperty && typeof value2 === "object") {
352
+ return value2[bindingProperty];
353
+ }
354
+ return value2;
355
+ })();
356
+ missing.splice(missing.indexOf(parameterName), 1);
357
+ if (value) {
358
+ const where = definition.wheres?.[parameterName];
359
+ if (where && !new RegExp(where).test(value)) {
360
+ console.warn(`[hybridly:routing] Parameter [${parameterName}] does not match the required format [${where}] for route [${routeName}].`);
361
+ }
362
+ return value;
315
363
  }
316
- const route = context.routing?.routes?.[name];
317
- if (!route) {
318
- throw new Error(`Route ${name.toString()} does not exist.`);
364
+ if (routing.defaults?.[parameterName]) {
365
+ return routing.defaults?.[parameterName];
319
366
  }
320
- return route;
321
- }
322
- get template() {
323
- const context = getInternalRouterContext();
324
- 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;
325
- return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
326
- }
327
- get parameterSegments() {
328
- return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
329
- name: segment.replace(/{|\??}/g, ""),
330
- required: !/\?}$/.test(segment)
331
- })) ?? [];
332
- }
333
- matchesUrl(url) {
334
- if (!this.definition.method.includes("GET")) {
335
- return false;
367
+ if (optional) {
368
+ return "";
336
369
  }
337
- const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
338
- const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
339
- return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
340
- }).replace(/^\w+:\/\//, "");
341
- const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
342
- const matches = new RegExp(`^${pattern}/?$`).exec(location);
343
- return matches ? { params: matches.groups, query: parse(query) } : false;
344
- }
345
- compile(params) {
346
- const segments = this.parameterSegments;
347
- if (!segments.length) {
348
- return this.template;
370
+ if (shouldThrow === false) {
371
+ return "";
349
372
  }
350
- return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
351
- if (!optional && [null, void 0].includes(params?.[segment])) {
352
- throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
353
- }
354
- if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
355
- return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
356
- }
357
- if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
358
- throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
359
- }
360
- return encodeURIComponent(params[segment] ?? "");
361
- }).replace(/\/+$/, "");
373
+ throw new MissingRouteParameter(parameterName, routeName);
374
+ });
375
+ const remaining = Object.keys(parameters).filter((key) => missing.includes(key)).reduce((obj, key) => ({
376
+ ...obj,
377
+ [key]: parameters[key]
378
+ }), {});
379
+ return {
380
+ pathname: path,
381
+ search: qs.stringify(remaining, {
382
+ encodeValuesOnly: true,
383
+ arrayFormat: "indices",
384
+ addQueryPrefix: true
385
+ })
386
+ };
387
+ }
388
+ function getRouteDefinition(name) {
389
+ const routing = getRouting();
390
+ const definition = routing.routes[name];
391
+ if (!definition) {
392
+ throw new RouteNotFound(name);
362
393
  }
394
+ return definition;
363
395
  }
364
-
365
- class Router extends String {
366
- constructor(name, parameters, absolute = true) {
367
- super();
368
- const context = getInternalRouterContext();
369
- this.route = new Route(name, absolute);
370
- this.routing = context.routing;
371
- this.setParameters(parameters);
372
- }
373
- toString() {
374
- 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] }), {});
375
- return this.route.compile(this.parameters) + stringify({ ...unhandled, ...this.parameters._query }, {
376
- addQueryPrefix: true,
377
- arrayFormat: "indices",
378
- encodeValuesOnly: true,
379
- skipNulls: true,
380
- encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
381
- });
396
+ function getRouting() {
397
+ const { routing } = getInternalRouterContext();
398
+ if (!routing) {
399
+ throw new RoutingNotInitialized();
382
400
  }
383
- static has(name) {
401
+ return routing;
402
+ }
403
+
404
+ function isCurrentFromName(name, parameters, mode = "loose") {
405
+ const location = window.location;
406
+ const matchee = (() => {
384
407
  try {
385
- Route.getDefinition(name);
386
- return true;
387
- } catch {
388
- return false;
408
+ return makeUrl(generateRouteFromName(name, parameters, true, false));
409
+ } catch (error) {
389
410
  }
411
+ })();
412
+ if (!matchee) {
413
+ return false;
390
414
  }
391
- setParameters(parameters) {
392
- this.parameters = parameters ?? {};
393
- this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
394
- const segments = this.route.parameterSegments.filter(({ name }) => !this.routing.defaults[name]);
395
- if (Array.isArray(this.parameters)) {
396
- this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
397
- } 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"))) {
398
- this.parameters = { [segments[0].name]: this.parameters };
399
- }
400
- this.parameters = {
401
- ...this.getDefaults(),
402
- ...this.substituteBindings()
403
- };
404
- }
405
- getDefaults() {
406
- return this.route.parameterSegments.filter(({ name }) => this.routing.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: this.routing.defaults[name] }), {});
407
- }
408
- substituteBindings() {
409
- return Object.entries(this.parameters).reduce((result, [key, value]) => {
410
- if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
411
- return { ...result, [key]: value };
412
- }
413
- if (!Reflect.has(value, this.route.definition.bindings[key])) {
414
- if (Reflect.has(value, "id")) {
415
- this.route.definition.bindings[key] = "id";
416
- } else {
417
- throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
418
- }
419
- }
420
- return { ...result, [key]: value[this.route.definition.bindings[key]] };
421
- }, {});
422
- }
423
- valueOf() {
424
- return this.toString();
415
+ if (mode === "strict") {
416
+ return location.href === matchee.href;
425
417
  }
418
+ return location.href.startsWith(matchee.href);
426
419
  }
427
420
 
428
421
  function route(name, parameters, absolute) {
429
- return new Router(name, parameters, absolute).toString();
422
+ return generateRouteFromName(name, parameters, absolute);
423
+ }
424
+ function current(name, parameters, mode = "loose") {
425
+ return isCurrentFromName(name, parameters, mode);
430
426
  }
431
427
  function updateRoutingConfiguration(routing) {
432
428
  if (!routing) {
@@ -540,8 +536,8 @@ const router = {
540
536
  local: async (url, options) => await performLocalNavigation(url, options),
541
537
  external: (url, data = {}) => navigateToExternalUrl(url, data),
542
538
  to: async (name, parameters, options) => {
543
- const url = new Router(name, parameters).toString();
544
- const method = Route.getDefinition(name).method.at(0);
539
+ const url = generateRouteFromName(name, parameters);
540
+ const method = getRouteDefinition(name).method.at(0);
545
541
  return await performHybridNavigation({ url, ...options, method });
546
542
  },
547
543
  history: {
@@ -589,8 +585,7 @@ async function performHybridNavigation(options) {
589
585
  }
590
586
  saveScrollPositions();
591
587
  if (options.url && options.transformUrl) {
592
- const transformUrl = typeof options.transformUrl === "function" ? options.transformUrl(makeUrl(options.url).toString()) : options.transformUrl;
593
- options.url = makeUrl(options.url, transformUrl);
588
+ options.url = makeUrl(options.url, options.transformUrl);
594
589
  }
595
590
  const targetUrl = makeUrl(options.url ?? context.url);
596
591
  const abortController = new AbortController();
@@ -815,4 +810,4 @@ function can(resource, action) {
815
810
  return resource.authorization?.[action] ?? false;
816
811
  }
817
812
 
818
- export { can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
813
+ export { can, constants, createRouter, current, 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.19",
3
+ "version": "0.0.1-alpha.20",
4
4
  "description": "A solution to develop server-driven, client-rendered applications",
5
5
  "keywords": [
6
6
  "hybridly",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "qs": "^6.11.0",
40
- "@hybridly/utils": "0.0.1-alpha.19"
40
+ "@hybridly/utils": "0.0.1-alpha.20"
41
41
  },
42
42
  "devDependencies": {
43
43
  "defu": "^6.1.1"