@rangojs/router 0.0.0-experimental.139 → 0.0.0-experimental.140
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/bin/rango.js +27 -2
- package/dist/vite/index.js +147 -30
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +1 -0
- package/skills/caching/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +15 -0
- package/skills/migrate-react-router/SKILL.md +15 -2
- package/skills/ppr/SKILL.md +426 -0
- package/skills/rango/SKILL.md +28 -25
- package/skills/route/SKILL.md +43 -0
- package/src/build/route-trie.ts +35 -7
- package/src/cache/cf/cf-cache-store.ts +155 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory-segment-store.ts +57 -1
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/types.ts +58 -0
- package/src/cache/vercel/vercel-cache-store.ts +159 -5
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +17 -0
- package/src/router/middleware.ts +14 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/segment-resolution/fresh.ts +12 -1
- package/src/router/segment-resolution/loader-cache.ts +14 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/rsc-rendering.ts +105 -51
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/types.ts +26 -0
- package/src/server/cookie-store.ts +45 -0
- package/src/server/live.ts +130 -0
- package/src/server/request-context.ts +49 -0
- package/src/ssr/index.tsx +377 -180
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/render-route.tsx +7 -9
- package/src/types/route-config.ts +19 -7
- package/src/urls/type-extraction.ts +43 -18
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/plugins/virtual-entries.ts +27 -2
- package/src/vite/router-discovery.ts +69 -15
- package/src/vite/utils/prerender-utils.ts +17 -4
package/dist/bin/rango.js
CHANGED
|
@@ -1437,9 +1437,14 @@ initializeApp().catch(console.error);
|
|
|
1437
1437
|
`.trim();
|
|
1438
1438
|
VIRTUAL_ENTRY_SSR = `
|
|
1439
1439
|
import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
1440
|
-
import { renderToReadableStream } from "react-dom/server.edge";
|
|
1440
|
+
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
1441
|
+
import { prerender } from "react-dom/static.edge";
|
|
1441
1442
|
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
1442
|
-
import {
|
|
1443
|
+
import {
|
|
1444
|
+
createSSRHandler,
|
|
1445
|
+
createShellCaptureHandler,
|
|
1446
|
+
createShellResumeHandler,
|
|
1447
|
+
} from "@rangojs/router/ssr";
|
|
1443
1448
|
|
|
1444
1449
|
export const renderHTML = createSSRHandler({
|
|
1445
1450
|
createFromReadableStream,
|
|
@@ -1448,6 +1453,26 @@ export const renderHTML = createSSRHandler({
|
|
|
1448
1453
|
loadBootstrapScriptContent: () =>
|
|
1449
1454
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1450
1455
|
});
|
|
1456
|
+
|
|
1457
|
+
export const captureShellHTML = createShellCaptureHandler({
|
|
1458
|
+
createFromReadableStream,
|
|
1459
|
+
renderToReadableStream,
|
|
1460
|
+
injectRSCPayload,
|
|
1461
|
+
prerender,
|
|
1462
|
+
resume,
|
|
1463
|
+
loadBootstrapScriptContent: () =>
|
|
1464
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
export const resumeShellHTML = createShellResumeHandler({
|
|
1468
|
+
createFromReadableStream,
|
|
1469
|
+
renderToReadableStream,
|
|
1470
|
+
injectRSCPayload,
|
|
1471
|
+
prerender,
|
|
1472
|
+
resume,
|
|
1473
|
+
loadBootstrapScriptContent: () =>
|
|
1474
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1475
|
+
});
|
|
1451
1476
|
`.trim();
|
|
1452
1477
|
VIRTUAL_IDS = {
|
|
1453
1478
|
browser: "virtual:rsc-router/entry.browser.js",
|
package/dist/vite/index.js
CHANGED
|
@@ -2212,9 +2212,14 @@ initializeApp().catch(console.error);
|
|
|
2212
2212
|
`.trim();
|
|
2213
2213
|
var VIRTUAL_ENTRY_SSR = `
|
|
2214
2214
|
import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
2215
|
-
import { renderToReadableStream } from "react-dom/server.edge";
|
|
2215
|
+
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
2216
|
+
import { prerender } from "react-dom/static.edge";
|
|
2216
2217
|
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
2217
|
-
import {
|
|
2218
|
+
import {
|
|
2219
|
+
createSSRHandler,
|
|
2220
|
+
createShellCaptureHandler,
|
|
2221
|
+
createShellResumeHandler,
|
|
2222
|
+
} from "@rangojs/router/ssr";
|
|
2218
2223
|
|
|
2219
2224
|
export const renderHTML = createSSRHandler({
|
|
2220
2225
|
createFromReadableStream,
|
|
@@ -2223,6 +2228,26 @@ export const renderHTML = createSSRHandler({
|
|
|
2223
2228
|
loadBootstrapScriptContent: () =>
|
|
2224
2229
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2225
2230
|
});
|
|
2231
|
+
|
|
2232
|
+
export const captureShellHTML = createShellCaptureHandler({
|
|
2233
|
+
createFromReadableStream,
|
|
2234
|
+
renderToReadableStream,
|
|
2235
|
+
injectRSCPayload,
|
|
2236
|
+
prerender,
|
|
2237
|
+
resume,
|
|
2238
|
+
loadBootstrapScriptContent: () =>
|
|
2239
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
export const resumeShellHTML = createShellResumeHandler({
|
|
2243
|
+
createFromReadableStream,
|
|
2244
|
+
renderToReadableStream,
|
|
2245
|
+
injectRSCPayload,
|
|
2246
|
+
prerender,
|
|
2247
|
+
resume,
|
|
2248
|
+
loadBootstrapScriptContent: () =>
|
|
2249
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2250
|
+
});
|
|
2226
2251
|
`.trim();
|
|
2227
2252
|
var RSC_ENTRY_BOOTSTRAP_IMPORTS = [
|
|
2228
2253
|
"virtual:rsc-router/routes-manifest",
|
|
@@ -4874,10 +4899,31 @@ var INTERNAL_RANGO_DEBUG = typeof process !== "undefined" && Boolean(process.env
|
|
|
4874
4899
|
// src/router/logging.ts
|
|
4875
4900
|
var routerLogContext = new AsyncLocalStorage();
|
|
4876
4901
|
|
|
4877
|
-
// src/router/
|
|
4902
|
+
// src/router/url-params.ts
|
|
4903
|
+
var PATH_SAFE_ESCAPES = {
|
|
4904
|
+
"%3A": ":",
|
|
4905
|
+
"%40": "@",
|
|
4906
|
+
"%24": "$",
|
|
4907
|
+
"%26": "&",
|
|
4908
|
+
"%2B": "+",
|
|
4909
|
+
"%2C": ",",
|
|
4910
|
+
"%3B": ";",
|
|
4911
|
+
"%3D": "="
|
|
4912
|
+
};
|
|
4913
|
+
function encodePathSegment(value) {
|
|
4914
|
+
return encodeURIComponent(value).replace(
|
|
4915
|
+
/%(?:3A|40|24|26|2B|2C|3B|3D)/gi,
|
|
4916
|
+
(match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
function encodePathRemainder(value, encode = encodePathSegment) {
|
|
4920
|
+
return value.split("/").map(encode).join("/");
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
// src/router/parse-pattern.ts
|
|
4878
4924
|
function parsePattern(pattern) {
|
|
4879
4925
|
const segments = [];
|
|
4880
|
-
const segmentRegex = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
4926
|
+
const segmentRegex = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([+*])?([^/]*)|(\*)|([^/]+))/g;
|
|
4881
4927
|
let match;
|
|
4882
4928
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
4883
4929
|
const [
|
|
@@ -4887,6 +4933,7 @@ function parsePattern(pattern) {
|
|
|
4887
4933
|
,
|
|
4888
4934
|
constraint,
|
|
4889
4935
|
optional,
|
|
4936
|
+
repeat,
|
|
4890
4937
|
suffix,
|
|
4891
4938
|
wildcard,
|
|
4892
4939
|
staticText
|
|
@@ -4894,17 +4941,38 @@ function parsePattern(pattern) {
|
|
|
4894
4941
|
if (wildcard) {
|
|
4895
4942
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
4896
4943
|
} else if (paramName) {
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4944
|
+
if (repeat && !suffix && optional !== "?" && !constraint) {
|
|
4945
|
+
segments.push({
|
|
4946
|
+
type: "wildcard",
|
|
4947
|
+
value: paramName,
|
|
4948
|
+
optional: false,
|
|
4949
|
+
oneOrMore: repeat === "+"
|
|
4950
|
+
});
|
|
4951
|
+
} else {
|
|
4952
|
+
segments.push({
|
|
4953
|
+
type: "param",
|
|
4954
|
+
value: paramName,
|
|
4955
|
+
optional: optional === "?",
|
|
4956
|
+
constraint: constraint ? constraint.split("|") : void 0,
|
|
4957
|
+
// Fold a non-modifier `+`/`*` back into the literal suffix.
|
|
4958
|
+
suffix: (repeat ?? "") + (suffix ?? "") || void 0
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4904
4961
|
} else if (staticText) {
|
|
4905
4962
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
4906
4963
|
}
|
|
4907
4964
|
}
|
|
4965
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
4966
|
+
const s = segments[i];
|
|
4967
|
+
if (s.type === "wildcard" && s.value !== "*") {
|
|
4968
|
+
segments[i] = {
|
|
4969
|
+
type: "param",
|
|
4970
|
+
value: s.value,
|
|
4971
|
+
optional: false,
|
|
4972
|
+
suffix: s.oneOrMore ? "+" : "*"
|
|
4973
|
+
};
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4908
4976
|
return segments;
|
|
4909
4977
|
}
|
|
4910
4978
|
|
|
@@ -5107,11 +5175,18 @@ function insertSegments(node, segments, index, leafBase, paramNames) {
|
|
|
5107
5175
|
} else if (segment.type === "wildcard") {
|
|
5108
5176
|
const wildLeaf = {
|
|
5109
5177
|
...buildLeaf(leafBase, paramNames),
|
|
5110
|
-
pn:
|
|
5178
|
+
pn: segment.value,
|
|
5179
|
+
...segment.oneOrMore ? { w1: true } : {}
|
|
5111
5180
|
};
|
|
5112
|
-
const existing = node.w
|
|
5113
|
-
const
|
|
5114
|
-
|
|
5181
|
+
const existing = node.w;
|
|
5182
|
+
const canMerge = existing === void 0 || Boolean(existing.rt) || Boolean(wildLeaf.rt) || existing.pn === wildLeaf.pn && Boolean(existing.w1) === Boolean(wildLeaf.w1);
|
|
5183
|
+
if (canMerge) {
|
|
5184
|
+
const merged = mergeLeaves(
|
|
5185
|
+
existing ? { ...existing } : void 0,
|
|
5186
|
+
wildLeaf
|
|
5187
|
+
);
|
|
5188
|
+
node.w = merged;
|
|
5189
|
+
}
|
|
5115
5190
|
}
|
|
5116
5191
|
}
|
|
5117
5192
|
|
|
@@ -5213,13 +5288,23 @@ import {
|
|
|
5213
5288
|
} from "node:fs";
|
|
5214
5289
|
import { resolve as resolve7 } from "node:path";
|
|
5215
5290
|
function encodePathParam(value) {
|
|
5216
|
-
return String(value)
|
|
5291
|
+
return encodePathRemainder(String(value), encodeURIComponent);
|
|
5217
5292
|
}
|
|
5218
5293
|
function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
|
|
5219
5294
|
let result = pattern;
|
|
5220
5295
|
let hadOmittedOptional = false;
|
|
5221
5296
|
for (const [key, value] of Object.entries(params)) {
|
|
5222
5297
|
const escaped = escapeRegExp(key);
|
|
5298
|
+
const catchAllRe = new RegExp(`:${escaped}[+*]`);
|
|
5299
|
+
if (catchAllRe.test(result)) {
|
|
5300
|
+
if (value === "") {
|
|
5301
|
+
result = result.replace(catchAllRe, "");
|
|
5302
|
+
hadOmittedOptional = true;
|
|
5303
|
+
} else {
|
|
5304
|
+
result = result.replace(catchAllRe, encodePathRemainder(value, encode));
|
|
5305
|
+
}
|
|
5306
|
+
continue;
|
|
5307
|
+
}
|
|
5223
5308
|
if (value === "") {
|
|
5224
5309
|
result = result.replace(
|
|
5225
5310
|
new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
|
|
@@ -5799,6 +5884,28 @@ var DiscoveryError = class _DiscoveryError extends Error {
|
|
|
5799
5884
|
Object.setPrototypeOf(this, _DiscoveryError.prototype);
|
|
5800
5885
|
}
|
|
5801
5886
|
};
|
|
5887
|
+
function describeDiscoveryFailure(err, opts = {}) {
|
|
5888
|
+
if (err instanceof DiscoveryError && err.caught.length === 0) {
|
|
5889
|
+
const entry = err.entryPath ?? "the router entry";
|
|
5890
|
+
if (opts.reoptimizeObserved) {
|
|
5891
|
+
return {
|
|
5892
|
+
level: "warn",
|
|
5893
|
+
message: `[rango] No routers found while Vite was re-optimizing dependencies on dev boot. This is transient: routes are served per-request and discovery re-runs automatically, so it clears on the next boot. If routes still 404, confirm ${entry} calls createRouter().`
|
|
5894
|
+
};
|
|
5895
|
+
}
|
|
5896
|
+
return {
|
|
5897
|
+
level: "error",
|
|
5898
|
+
message: `${err.message}
|
|
5899
|
+
Ensure ${entry} calls createRouter() at module top level and that the configured router entry path is correct.`
|
|
5900
|
+
};
|
|
5901
|
+
}
|
|
5902
|
+
const e = err;
|
|
5903
|
+
const detail = e?.stack ?? e?.message ?? String(err);
|
|
5904
|
+
return {
|
|
5905
|
+
level: "error",
|
|
5906
|
+
message: `[rango] Router discovery failed: ${detail}`
|
|
5907
|
+
};
|
|
5908
|
+
}
|
|
5802
5909
|
|
|
5803
5910
|
// src/vite/discovery/discover-routers.ts
|
|
5804
5911
|
var debug10 = createRangoDebugger(NS.discovery);
|
|
@@ -6861,6 +6968,15 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
6861
6968
|
await importEntryAndRegistry(tempRscEnv);
|
|
6862
6969
|
return tempRscEnv;
|
|
6863
6970
|
}
|
|
6971
|
+
const emitDiscoveryFailure = (err, hashBefore, hashAfter) => {
|
|
6972
|
+
const reoptimizeObserved = hashBefore !== void 0 && hashAfter !== void 0 && hashBefore !== hashAfter;
|
|
6973
|
+
const report = describeDiscoveryFailure(err, { reoptimizeObserved });
|
|
6974
|
+
if (report.level === "warn") {
|
|
6975
|
+
console.warn(report.message);
|
|
6976
|
+
} else {
|
|
6977
|
+
console.error(report.message);
|
|
6978
|
+
}
|
|
6979
|
+
};
|
|
6864
6980
|
const discover = async () => {
|
|
6865
6981
|
const discoverStart = performance.now();
|
|
6866
6982
|
const rscEnv = server.environments?.rsc;
|
|
@@ -6870,18 +6986,21 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
6870
6986
|
globalThis.__rscRouterDiscoveryActive ?? false
|
|
6871
6987
|
);
|
|
6872
6988
|
s.devServerOrigin = getDevServerOrigin();
|
|
6989
|
+
let tempRscEnv;
|
|
6990
|
+
let optimizerHashBefore2;
|
|
6873
6991
|
try {
|
|
6874
6992
|
await timed(
|
|
6875
6993
|
debugDiscovery,
|
|
6876
6994
|
"acquireBuildEnv",
|
|
6877
6995
|
() => acquireBuildEnv(s, viteCommand, viteMode)
|
|
6878
6996
|
);
|
|
6879
|
-
|
|
6997
|
+
tempRscEnv = await timed(
|
|
6880
6998
|
debugDiscovery,
|
|
6881
6999
|
"getOrCreateTempServer",
|
|
6882
7000
|
() => getOrCreateTempServer()
|
|
6883
7001
|
);
|
|
6884
7002
|
if (tempRscEnv) {
|
|
7003
|
+
optimizerHashBefore2 = tempRscEnv.depsOptimizer?.metadata?.browserHash;
|
|
6885
7004
|
await timed(
|
|
6886
7005
|
debugDiscovery,
|
|
6887
7006
|
"discoverRouters (cloudflare)",
|
|
@@ -6894,9 +7013,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
6894
7013
|
);
|
|
6895
7014
|
}
|
|
6896
7015
|
} catch (err) {
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
7016
|
+
emitDiscoveryFailure(
|
|
7017
|
+
err,
|
|
7018
|
+
optimizerHashBefore2,
|
|
7019
|
+
tempRscEnv?.depsOptimizer?.metadata?.browserHash
|
|
6900
7020
|
);
|
|
6901
7021
|
}
|
|
6902
7022
|
debugDiscovery?.(
|
|
@@ -6906,6 +7026,7 @@ ${err.stack}`
|
|
|
6906
7026
|
resolveDiscovery();
|
|
6907
7027
|
return;
|
|
6908
7028
|
}
|
|
7029
|
+
const optimizerHashBefore = rscEnv.depsOptimizer?.metadata?.browserHash;
|
|
6909
7030
|
try {
|
|
6910
7031
|
debugDiscovery?.("dev: node path start");
|
|
6911
7032
|
await timed(
|
|
@@ -6915,17 +7036,12 @@ ${err.stack}`
|
|
|
6915
7036
|
);
|
|
6916
7037
|
const serverMod = await timed(
|
|
6917
7038
|
debugDiscovery,
|
|
6918
|
-
"
|
|
6919
|
-
() => rscEnv
|
|
7039
|
+
"discoverRouters",
|
|
7040
|
+
() => discoverRouters(s, rscEnv)
|
|
6920
7041
|
);
|
|
6921
7042
|
if (serverMod?.setManifestReadyPromise) {
|
|
6922
7043
|
serverMod.setManifestReadyPromise(discoveryPromise);
|
|
6923
7044
|
}
|
|
6924
|
-
await timed(
|
|
6925
|
-
debugDiscovery,
|
|
6926
|
-
"discoverRouters",
|
|
6927
|
-
() => discoverRouters(s, rscEnv)
|
|
6928
|
-
);
|
|
6929
7045
|
s.devServerOrigin = getDevServerOrigin();
|
|
6930
7046
|
timedSync(
|
|
6931
7047
|
debugDiscovery,
|
|
@@ -6938,9 +7054,10 @@ ${err.stack}`
|
|
|
6938
7054
|
() => propagateDiscoveryState(rscEnv)
|
|
6939
7055
|
);
|
|
6940
7056
|
} catch (err) {
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
7057
|
+
emitDiscoveryFailure(
|
|
7058
|
+
err,
|
|
7059
|
+
optimizerHashBefore,
|
|
7060
|
+
rscEnv.depsOptimizer?.metadata?.browserHash
|
|
6944
7061
|
);
|
|
6945
7062
|
} finally {
|
|
6946
7063
|
debugDiscovery?.(
|
package/package.json
CHANGED
|
@@ -484,3 +484,4 @@ overrides — see `/loader` for the full reference.
|
|
|
484
484
|
- `/caching` — cache() DSL setup, stores, nested boundaries
|
|
485
485
|
- `/use-cache` — "use cache" directive details, profiles, transforms, guards
|
|
486
486
|
- `/document-cache` — Edge caching with Cache-Control headers (different layer)
|
|
487
|
+
- `/ppr` — PPR shell caching: cached HTML shell + live loader holes (different layer)
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -88,6 +88,21 @@ The Document component replaces `app/layout.tsx`'s `<html>` wrapper. See `/route
|
|
|
88
88
|
| `app/shop/[...path]/page.tsx` | `path("/shop/:path+", CatchAll, { name: "shopCatchAll" })` |
|
|
89
89
|
| `app/docs/[[...slug]]/page.tsx` | `path("/docs/:slug*", Docs, { name: "docs" })` |
|
|
90
90
|
|
|
91
|
+
The catch-all remainder is a single string at `ctx.params.<name>` with the `/`
|
|
92
|
+
separators preserved — split it to recover the array Next gives you:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// app/docs/[[...slug]]/page.tsx -> params.slug is string[] | undefined in Next
|
|
96
|
+
path("/docs/:slug*", (ctx) => {
|
|
97
|
+
// "" for /docs, "a/b/c" for /docs/a/b/c
|
|
98
|
+
const slug = ctx.params.slug === "" ? [] : ctx.params.slug.split("/");
|
|
99
|
+
return <Docs slug={slug} />;
|
|
100
|
+
}, { name: "docs" });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`[...path]` (required, ≥1 segment) maps to `:path+`; `[[...slug]]` (optional,
|
|
104
|
+
matches the bare parent too) maps to `:slug*` — which binds `""` at `/docs`.
|
|
105
|
+
|
|
91
106
|
### Layouts
|
|
92
107
|
|
|
93
108
|
```typescript
|
|
@@ -199,7 +199,7 @@ Key shift: the route module's scattered exports consolidate into the handler
|
|
|
199
199
|
| `app/routes/_index.tsx` | `path("/", HomePage, { name: "home" })` |
|
|
200
200
|
| `app/routes/about.tsx` | `path("/about", AboutPage, { name: "about" })` |
|
|
201
201
|
| `app/routes/blog.$slug.tsx` | `path("/blog/:slug", BlogPost, { name: "blogPost" })` |
|
|
202
|
-
| `app/routes/files.$.tsx` (splat) | `path("/files/:path
|
|
202
|
+
| `app/routes/files.$.tsx` (splat) | `path("/files/:path*", FileBrowser, { name: "files" })` |
|
|
203
203
|
| `app/routes/dashboard.tsx` (layout) | `layout(<DashboardLayout />, () => [...])` |
|
|
204
204
|
| `app/routes/dashboard._index.tsx` | `path("/dashboard", DashboardIndex, { name: "dashboard" })` |
|
|
205
205
|
| `app/routes/dashboard.settings.tsx` | `path("/dashboard/settings", Settings, { name: "settings" })` |
|
|
@@ -213,9 +213,22 @@ Key shift: the route module's scattered exports consolidate into the handler
|
|
|
213
213
|
| `path: "/"` | `path("/", HomePage, { name: "home" })` |
|
|
214
214
|
| `path: "about"` | `path("/about", AboutPage, { name: "about" })` |
|
|
215
215
|
| `path: "blog/:slug"` | `path("/blog/:slug", BlogPost, { name: "blogPost" })` |
|
|
216
|
-
| `path: "files/*"` (splat) | `path("/files/:path
|
|
216
|
+
| `path: "files/*"` (splat) | `path("/files/:path*", FileBrowser, { name: "files" })` |
|
|
217
217
|
| `path: "docs/:lang?"` (optional param) | `path("/docs/:lang?", Docs, { name: "docs" })` |
|
|
218
218
|
|
|
219
|
+
The RR splat (`$` / `*`) matches the bare parent too (`/files` binds `""`), so
|
|
220
|
+
it maps to `:path*` (zero-or-more). Use `:path+` only when you require at least
|
|
221
|
+
one trailing segment. RR reads the splat at `params["*"]`; Rango exposes it as a
|
|
222
|
+
named string at `ctx.params.path` with the `/` separators preserved (split to
|
|
223
|
+
recover RR's array):
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
path("/files/:path*", (ctx) => {
|
|
227
|
+
const parts = ctx.params.path === "" ? [] : ctx.params.path.split("/");
|
|
228
|
+
return <FileBrowser path={parts} />;
|
|
229
|
+
}, { name: "files" });
|
|
230
|
+
```
|
|
231
|
+
|
|
219
232
|
### Layouts
|
|
220
233
|
|
|
221
234
|
React Router layouts use `<Outlet />` — same concept in Rango:
|