@jsenv/navi 0.16.0 → 0.16.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/jsenv_navi.js +230 -23
- package/dist/jsenv_navi.js.map +4 -4
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -7522,21 +7522,31 @@ setBaseUrl(
|
|
|
7522
7522
|
// Pattern registry for building relationships before routes are created
|
|
7523
7523
|
const patternRegistry = new Map(); // pattern -> patternData
|
|
7524
7524
|
const patternRelationships = new Map(); // pattern -> relationships
|
|
7525
|
+
let patternsRegistered = false;
|
|
7525
7526
|
|
|
7526
7527
|
// Function to detect signals in route patterns and connect them
|
|
7527
7528
|
const detectSignals = (routePattern) => {
|
|
7528
7529
|
const signalConnections = [];
|
|
7529
7530
|
let updatedPattern = routePattern;
|
|
7530
7531
|
|
|
7531
|
-
// Look for signals in
|
|
7532
|
-
//
|
|
7533
|
-
|
|
7532
|
+
// Look for signals in two formats:
|
|
7533
|
+
// 1. Expected format: :paramName={navi_state_signal:id} or ?paramName={navi_state_signal:id} or ¶mName={navi_state_signal:id}
|
|
7534
|
+
// 2. Typoe format (missing = sign): ¶mName{navi_state_signal:id}
|
|
7535
|
+
const signalParamRegex = /([?:&])(\w+)(=)?(\{navi_state_signal:[^}]+\})/g;
|
|
7534
7536
|
let match;
|
|
7535
7537
|
|
|
7536
7538
|
while ((match = signalParamRegex.exec(routePattern)) !== null) {
|
|
7537
|
-
const [fullMatch, prefix, paramName, signalString] = match;
|
|
7539
|
+
const [fullMatch, prefix, paramName, equalSign, signalString] = match;
|
|
7538
7540
|
|
|
7539
|
-
//
|
|
7541
|
+
// Emit warning if equal sign is missing
|
|
7542
|
+
if (!equalSign) {
|
|
7543
|
+
console.warn(
|
|
7544
|
+
`[detectSignals] Missing '=' sign in route pattern: "${prefix}${paramName}${signalString}". ` +
|
|
7545
|
+
`Consider using "${prefix}${paramName}=${signalString}" for better clarity.`,
|
|
7546
|
+
);
|
|
7547
|
+
}
|
|
7548
|
+
|
|
7549
|
+
// Extract the signal ID from the format: {navi_state_signal:id}
|
|
7540
7550
|
const signalIdMatch = signalString.match(/\{navi_state_signal:([^}]+)\}/);
|
|
7541
7551
|
if (!signalIdMatch) {
|
|
7542
7552
|
console.warn(
|
|
@@ -7553,13 +7563,10 @@ const detectSignals = (routePattern) => {
|
|
|
7553
7563
|
|
|
7554
7564
|
let replacement;
|
|
7555
7565
|
if (prefix === ":") {
|
|
7556
|
-
// Path parameter: :section=
|
|
7557
|
-
replacement = `${prefix}${paramName}`;
|
|
7558
|
-
} else if (prefix === "?") {
|
|
7559
|
-
// First search parameter: ?city=__jsenv_signal_1__ becomes ?city
|
|
7566
|
+
// Path parameter: :section={navi_state_signal:...} becomes :section
|
|
7560
7567
|
replacement = `${prefix}${paramName}`;
|
|
7561
|
-
} else if (prefix === "&") {
|
|
7562
|
-
//
|
|
7568
|
+
} else if (prefix === "?" || prefix === "&") {
|
|
7569
|
+
// Query parameter: ?city={navi_state_signal:...} or &lon{navi_state_signal:...} becomes ?city or &lon
|
|
7563
7570
|
replacement = `${prefix}${paramName}`;
|
|
7564
7571
|
}
|
|
7565
7572
|
updatedPattern = updatedPattern.replace(fullMatch, replacement);
|
|
@@ -7606,6 +7613,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7606
7613
|
const result = matchUrl(parsedPattern, url, {
|
|
7607
7614
|
parameterDefaults,
|
|
7608
7615
|
baseUrl,
|
|
7616
|
+
connections,
|
|
7609
7617
|
});
|
|
7610
7618
|
|
|
7611
7619
|
return result;
|
|
@@ -7793,6 +7801,41 @@ const createRoutePattern = (pattern) => {
|
|
|
7793
7801
|
}
|
|
7794
7802
|
}
|
|
7795
7803
|
|
|
7804
|
+
// PARENT PARAMETER INHERITANCE: Inherit query parameters from parent patterns
|
|
7805
|
+
// This allows child routes like "/map/isochrone" to inherit "zoom=15" from parent "/map/?zoom=..."
|
|
7806
|
+
const parentPatterns = relationships?.parentPatterns || [];
|
|
7807
|
+
for (const parentPattern of parentPatterns) {
|
|
7808
|
+
const parentPatternData = getPatternData(parentPattern);
|
|
7809
|
+
if (!parentPatternData) continue;
|
|
7810
|
+
|
|
7811
|
+
// Check parent's signal connections for non-default values to inherit
|
|
7812
|
+
for (const parentConnection of parentPatternData.connections) {
|
|
7813
|
+
const { paramName, signal, options } = parentConnection;
|
|
7814
|
+
const defaultValue = options.defaultValue;
|
|
7815
|
+
|
|
7816
|
+
// If we don't already have this parameter and parent signal has non-default value
|
|
7817
|
+
if (
|
|
7818
|
+
!(paramName in finalParams) &&
|
|
7819
|
+
signal?.value !== undefined &&
|
|
7820
|
+
signal.value !== defaultValue
|
|
7821
|
+
) {
|
|
7822
|
+
// Check if this parameter corresponds to a literal segment in our path
|
|
7823
|
+
// E.g., don't inherit "section=analytics" if our path is "/admin/analytics"
|
|
7824
|
+
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
7825
|
+
parsedPattern,
|
|
7826
|
+
parentPatternData.parsedPattern,
|
|
7827
|
+
paramName,
|
|
7828
|
+
signal.value,
|
|
7829
|
+
);
|
|
7830
|
+
|
|
7831
|
+
if (shouldInherit) {
|
|
7832
|
+
// Inherit the parent's signal value
|
|
7833
|
+
finalParams[paramName] = signal.value;
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7796
7839
|
if (!parsedPattern.segments) {
|
|
7797
7840
|
return "/";
|
|
7798
7841
|
}
|
|
@@ -7950,7 +7993,11 @@ const checkIfLiteralCanBeOptional = (literalValue, patternRegistry) => {
|
|
|
7950
7993
|
/**
|
|
7951
7994
|
* Match a URL against a parsed pattern
|
|
7952
7995
|
*/
|
|
7953
|
-
const matchUrl = (
|
|
7996
|
+
const matchUrl = (
|
|
7997
|
+
parsedPattern,
|
|
7998
|
+
url,
|
|
7999
|
+
{ parameterDefaults, baseUrl, connections = [] },
|
|
8000
|
+
) => {
|
|
7954
8001
|
// Parse the URL
|
|
7955
8002
|
const urlObj = new URL(url, baseUrl);
|
|
7956
8003
|
let pathname = urlObj.pathname;
|
|
@@ -7972,14 +8019,14 @@ const matchUrl = (parsedPattern, url, { parameterDefaults, baseUrl }) => {
|
|
|
7972
8019
|
// OR when URL exactly matches baseUrl (treating baseUrl as root)
|
|
7973
8020
|
if (parsedPattern.segments.length === 0) {
|
|
7974
8021
|
if (pathname === "/" || pathname === "") {
|
|
7975
|
-
return extractSearchParams(urlObj);
|
|
8022
|
+
return extractSearchParams(urlObj, connections);
|
|
7976
8023
|
}
|
|
7977
8024
|
|
|
7978
8025
|
// Special case: if URL exactly matches baseUrl, treat as root route
|
|
7979
8026
|
if (baseUrl) {
|
|
7980
8027
|
const baseUrlObj = new URL(baseUrl);
|
|
7981
8028
|
if (originalPathname === baseUrlObj.pathname) {
|
|
7982
|
-
return extractSearchParams(urlObj);
|
|
8029
|
+
return extractSearchParams(urlObj, connections);
|
|
7983
8030
|
}
|
|
7984
8031
|
}
|
|
7985
8032
|
|
|
@@ -8074,7 +8121,7 @@ const matchUrl = (parsedPattern, url, { parameterDefaults, baseUrl }) => {
|
|
|
8074
8121
|
// If pattern has trailing slash or wildcard, allow extra segments (no additional check needed)
|
|
8075
8122
|
|
|
8076
8123
|
// Add search parameters
|
|
8077
|
-
const searchParams = extractSearchParams(urlObj);
|
|
8124
|
+
const searchParams = extractSearchParams(urlObj, connections);
|
|
8078
8125
|
Object.assign(params, searchParams);
|
|
8079
8126
|
|
|
8080
8127
|
// Apply remaining parameter defaults for unmatched parameters
|
|
@@ -8090,10 +8137,29 @@ const matchUrl = (parsedPattern, url, { parameterDefaults, baseUrl }) => {
|
|
|
8090
8137
|
/**
|
|
8091
8138
|
* Extract search parameters from URL
|
|
8092
8139
|
*/
|
|
8093
|
-
const extractSearchParams = (urlObj) => {
|
|
8140
|
+
const extractSearchParams = (urlObj, connections = []) => {
|
|
8094
8141
|
const params = {};
|
|
8142
|
+
|
|
8143
|
+
// Create a map for quick signal type lookup
|
|
8144
|
+
const signalTypes = new Map();
|
|
8145
|
+
for (const connection of connections) {
|
|
8146
|
+
if (connection.options.type) {
|
|
8147
|
+
signalTypes.set(connection.paramName, connection.options.type);
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
|
|
8095
8151
|
for (const [key, value] of urlObj.searchParams) {
|
|
8096
|
-
|
|
8152
|
+
const signalType = signalTypes.get(key);
|
|
8153
|
+
|
|
8154
|
+
// Cast value based on signal type
|
|
8155
|
+
if (signalType === "number" || signalType === "float") {
|
|
8156
|
+
const numberValue = Number(value);
|
|
8157
|
+
params[key] = isNaN(numberValue) ? value : numberValue;
|
|
8158
|
+
} else if (signalType === "boolean") {
|
|
8159
|
+
params[key] = value === "true" || value === "1";
|
|
8160
|
+
} else {
|
|
8161
|
+
params[key] = value;
|
|
8162
|
+
}
|
|
8097
8163
|
}
|
|
8098
8164
|
return params;
|
|
8099
8165
|
};
|
|
@@ -8157,6 +8223,27 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
|
8157
8223
|
path = path.slice(0, -1);
|
|
8158
8224
|
}
|
|
8159
8225
|
|
|
8226
|
+
// Check if we'll have query parameters to decide on trailing slash removal
|
|
8227
|
+
const willHaveQueryParams =
|
|
8228
|
+
parsedPattern.queryParams?.some((qp) => {
|
|
8229
|
+
const value = params[qp.name];
|
|
8230
|
+
return value !== undefined;
|
|
8231
|
+
}) ||
|
|
8232
|
+
Object.entries(params).some(([key, value]) => {
|
|
8233
|
+
const isPathParam = parsedPattern.segments.some(
|
|
8234
|
+
(s) => s.type === "param" && s.name === key,
|
|
8235
|
+
);
|
|
8236
|
+
const isQueryParam = parsedPattern.queryParams?.some(
|
|
8237
|
+
(qp) => qp.name === key,
|
|
8238
|
+
);
|
|
8239
|
+
return value !== undefined && !isPathParam && !isQueryParam;
|
|
8240
|
+
});
|
|
8241
|
+
|
|
8242
|
+
// Remove trailing slash when we have query params for prettier URLs
|
|
8243
|
+
if (willHaveQueryParams && path.endsWith("/") && path !== "/") {
|
|
8244
|
+
path = path.slice(0, -1);
|
|
8245
|
+
}
|
|
8246
|
+
|
|
8160
8247
|
// Add search parameters
|
|
8161
8248
|
const pathParamNames = new Set(
|
|
8162
8249
|
parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
|
|
@@ -8255,6 +8342,43 @@ const isChildPattern = (childPattern, parentPattern) => {
|
|
|
8255
8342
|
return childSegments.length > parentSegments.length || hasMoreSpecificSegment;
|
|
8256
8343
|
};
|
|
8257
8344
|
|
|
8345
|
+
/**
|
|
8346
|
+
* Check if a parameter is redundant because the child pattern already has it as a literal segment
|
|
8347
|
+
* E.g., parameter "section" is redundant for pattern "/admin/settings/:tab" because "settings" is literal
|
|
8348
|
+
*/
|
|
8349
|
+
const isParameterRedundantWithLiteralSegments = (
|
|
8350
|
+
childPattern,
|
|
8351
|
+
parentPattern,
|
|
8352
|
+
paramName,
|
|
8353
|
+
) => {
|
|
8354
|
+
// Find which segment position corresponds to this parameter in the parent
|
|
8355
|
+
let paramSegmentIndex = -1;
|
|
8356
|
+
for (let i = 0; i < parentPattern.segments.length; i++) {
|
|
8357
|
+
const segment = parentPattern.segments[i];
|
|
8358
|
+
if (segment.type === "param" && segment.name === paramName) {
|
|
8359
|
+
paramSegmentIndex = i;
|
|
8360
|
+
break;
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8364
|
+
// If parameter not found in parent segments, it's not redundant with path
|
|
8365
|
+
if (paramSegmentIndex === -1) {
|
|
8366
|
+
return false;
|
|
8367
|
+
}
|
|
8368
|
+
|
|
8369
|
+
// Check if child has a literal segment at the same position
|
|
8370
|
+
if (childPattern.segments.length > paramSegmentIndex) {
|
|
8371
|
+
const childSegment = childPattern.segments[paramSegmentIndex];
|
|
8372
|
+
if (childSegment.type === "literal") {
|
|
8373
|
+
// Child has a literal segment where parent has parameter
|
|
8374
|
+
// This means the child is more specific and shouldn't inherit this parameter
|
|
8375
|
+
return true; // Redundant - child already specifies this position with a literal
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
|
|
8379
|
+
return false;
|
|
8380
|
+
};
|
|
8381
|
+
|
|
8258
8382
|
/**
|
|
8259
8383
|
* Register all patterns at once and build their relationships
|
|
8260
8384
|
*/
|
|
@@ -8309,6 +8433,8 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8309
8433
|
originalPattern: currentPattern,
|
|
8310
8434
|
});
|
|
8311
8435
|
}
|
|
8436
|
+
|
|
8437
|
+
patternsRegistered = true;
|
|
8312
8438
|
};
|
|
8313
8439
|
|
|
8314
8440
|
/**
|
|
@@ -8318,12 +8444,25 @@ const getPatternData = (urlPatternRaw) => {
|
|
|
8318
8444
|
return patternRegistry.get(urlPatternRaw);
|
|
8319
8445
|
};
|
|
8320
8446
|
|
|
8447
|
+
/**
|
|
8448
|
+
* Get pattern relationships for route creation
|
|
8449
|
+
*/
|
|
8450
|
+
const getPatternRelationships = () => {
|
|
8451
|
+
if (!patternsRegistered) {
|
|
8452
|
+
throw new Error(
|
|
8453
|
+
"Patterns must be registered before accessing relationships",
|
|
8454
|
+
);
|
|
8455
|
+
}
|
|
8456
|
+
return patternRelationships;
|
|
8457
|
+
};
|
|
8458
|
+
|
|
8321
8459
|
/**
|
|
8322
8460
|
* Clear all registered patterns
|
|
8323
8461
|
*/
|
|
8324
8462
|
const clearPatterns = () => {
|
|
8325
8463
|
patternRegistry.clear();
|
|
8326
8464
|
patternRelationships.clear();
|
|
8465
|
+
patternsRegistered = false;
|
|
8327
8466
|
};
|
|
8328
8467
|
|
|
8329
8468
|
const resolveRouteUrl = (relativeUrl) => {
|
|
@@ -8540,6 +8679,41 @@ const getRoutePrivateProperties = (route) => {
|
|
|
8540
8679
|
return routePrivatePropertiesMap.get(route);
|
|
8541
8680
|
};
|
|
8542
8681
|
|
|
8682
|
+
/**
|
|
8683
|
+
* Get child routes of a given route
|
|
8684
|
+
*/
|
|
8685
|
+
const getRouteChildren = (route) => {
|
|
8686
|
+
const children = [];
|
|
8687
|
+
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8688
|
+
if (!routePrivateProperties) {
|
|
8689
|
+
return children;
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8692
|
+
const { originalPattern } = routePrivateProperties;
|
|
8693
|
+
const relationships = getPatternRelationships();
|
|
8694
|
+
const relationshipData = relationships.get(originalPattern);
|
|
8695
|
+
|
|
8696
|
+
if (!relationshipData || !relationshipData.children) {
|
|
8697
|
+
return children;
|
|
8698
|
+
}
|
|
8699
|
+
|
|
8700
|
+
// Find child routes
|
|
8701
|
+
for (const childPattern of relationshipData.children) {
|
|
8702
|
+
for (const otherRoute of routeSet) {
|
|
8703
|
+
const otherRoutePrivateProperties = getRoutePrivateProperties(otherRoute);
|
|
8704
|
+
if (
|
|
8705
|
+
otherRoutePrivateProperties &&
|
|
8706
|
+
otherRoutePrivateProperties.originalPattern === childPattern
|
|
8707
|
+
) {
|
|
8708
|
+
children.push(otherRoute);
|
|
8709
|
+
break;
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8713
|
+
|
|
8714
|
+
return children;
|
|
8715
|
+
};
|
|
8716
|
+
|
|
8543
8717
|
const registerRoute = (routePattern) => {
|
|
8544
8718
|
const urlPatternRaw = routePattern.originalPattern;
|
|
8545
8719
|
const patternData = getPatternData(urlPatternRaw);
|
|
@@ -8626,6 +8800,13 @@ const registerRoute = (routePattern) => {
|
|
|
8626
8800
|
for (const { signal: stateSignal, paramName, options = {} } of connections) {
|
|
8627
8801
|
const { debug } = options;
|
|
8628
8802
|
|
|
8803
|
+
if (debug) {
|
|
8804
|
+
console.debug(
|
|
8805
|
+
`[route] connecting param "${paramName}" to signal`,
|
|
8806
|
+
stateSignal,
|
|
8807
|
+
);
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8629
8810
|
// URL -> Signal synchronization
|
|
8630
8811
|
effect(() => {
|
|
8631
8812
|
const matching = matchingSignal.value;
|
|
@@ -8684,13 +8865,39 @@ const registerRoute = (routePattern) => {
|
|
|
8684
8865
|
);
|
|
8685
8866
|
return null;
|
|
8686
8867
|
}
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8868
|
+
|
|
8869
|
+
// Walk down the hierarchy updating action params and tracking most specific route
|
|
8870
|
+
let currentRoute = route;
|
|
8871
|
+
let mostSpecificRoute;
|
|
8872
|
+
|
|
8873
|
+
while (currentRoute) {
|
|
8874
|
+
if (!currentRoute.matching) {
|
|
8875
|
+
break;
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8878
|
+
// Update the most specific route as we go
|
|
8879
|
+
mostSpecificRoute = currentRoute;
|
|
8880
|
+
// Update action params
|
|
8881
|
+
if (currentRoute.action) {
|
|
8882
|
+
const currentRoutePrivateProperties =
|
|
8883
|
+
getRoutePrivateProperties(currentRoute);
|
|
8884
|
+
if (currentRoutePrivateProperties) {
|
|
8885
|
+
const { routePattern: currentRoutePattern } =
|
|
8886
|
+
currentRoutePrivateProperties;
|
|
8887
|
+
const currentResolvedParams = currentRoutePattern.resolveParams();
|
|
8888
|
+
const updatedActionParams = {
|
|
8889
|
+
...currentResolvedParams,
|
|
8890
|
+
...newParams,
|
|
8891
|
+
};
|
|
8892
|
+
currentRoute.action.replaceParams(updatedActionParams);
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8895
|
+
|
|
8896
|
+
// Find the first matching child to continue down the hierarchy
|
|
8897
|
+
const children = getRouteChildren(currentRoute);
|
|
8898
|
+
currentRoute = children.find((child) => child.matching) || null;
|
|
8692
8899
|
}
|
|
8693
|
-
return
|
|
8900
|
+
return mostSpecificRoute.redirectTo(newParams);
|
|
8694
8901
|
};
|
|
8695
8902
|
route.buildRelativeUrl = (params) => {
|
|
8696
8903
|
// buildMostPreciseUrl now handles parameter resolution internally
|