@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 +114 -118
- package/dist/index.d.ts +10 -3
- package/dist/index.mjs +116 -121
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
throw new Error(`Route ${name.toString()} does not exist.`);
|
|
373
|
+
if (routing.defaults?.[parameterName]) {
|
|
374
|
+
return routing.defaults?.[parameterName];
|
|
328
375
|
}
|
|
329
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
410
|
+
return routing;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isCurrentFromName(name, parameters, mode = "loose") {
|
|
414
|
+
const location = window.location;
|
|
415
|
+
const matchee = (() => {
|
|
393
416
|
try {
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
|
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 =
|
|
553
|
-
const method =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
throw new Error(`Route ${name.toString()} does not exist.`);
|
|
364
|
+
if (routing.defaults?.[parameterName]) {
|
|
365
|
+
return routing.defaults?.[parameterName];
|
|
319
366
|
}
|
|
320
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
401
|
+
return routing;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isCurrentFromName(name, parameters, mode = "loose") {
|
|
405
|
+
const location = window.location;
|
|
406
|
+
const matchee = (() => {
|
|
384
407
|
try {
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
|
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 =
|
|
544
|
-
const method =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
40
|
+
"@hybridly/utils": "0.0.1-alpha.20"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"defu": "^6.1.1"
|