@rangojs/router 0.0.0-experimental.132 → 0.0.0-experimental.133
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/AGENTS.md +8 -0
- package/README.md +43 -2
- package/dist/bin/rango.js +92 -16
- package/dist/vite/index.js +166 -70
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +16 -9
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +2 -2
- package/skills/host-router/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +1 -1
- package/skills/loader/SKILL.md +2 -0
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/prerender/SKILL.md +2 -0
- package/skills/rango/SKILL.md +12 -11
- package/skills/response-routes/SKILL.md +2 -2
- package/skills/route/SKILL.md +4 -0
- package/skills/router-setup/SKILL.md +3 -0
- package/skills/scripts/SKILL.md +179 -0
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +20 -6
- package/skills/testing/cache-prerender.md +5 -2
- package/skills/testing/client-components.md +2 -0
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +8 -9
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/server-actions.md +11 -11
- package/skills/testing/setup.md +3 -0
- package/skills/typesafety/SKILL.md +3 -2
- package/skills/use-cache/SKILL.md +10 -9
- package/src/browser/event-controller.ts +109 -2
- package/src/browser/partial-update.ts +12 -0
- package/src/browser/prefetch/cache.ts +17 -0
- package/src/browser/prefetch/fetch.ts +69 -2
- package/src/browser/react/Link.tsx +30 -5
- package/src/browser/react/NavigationProvider.tsx +12 -2
- package/src/browser/react/location-state-shared.ts +14 -2
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +23 -2
- package/src/browser/response-adapter.ts +14 -3
- package/src/browser/rsc-router.tsx +3 -0
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +46 -11
- package/src/browser/types.ts +6 -0
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-trie.ts +33 -9
- package/src/build/route-types/include-resolution.ts +7 -1
- package/src/build/route-types/router-processing.ts +0 -6
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-policy.ts +42 -8
- package/src/cache/cache-runtime.ts +65 -5
- package/src/cache/cache-scope.ts +71 -11
- package/src/cache/cache-tag.ts +7 -2
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +85 -613
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/document-cache.ts +11 -0
- package/src/cache/handle-snapshot.ts +8 -1
- package/src/cache/profile-registry.ts +25 -1
- package/src/cache/segment-codec.ts +9 -1
- package/src/cache/types.ts +4 -0
- package/src/client.rsc.tsx +38 -0
- package/src/client.tsx +11 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +1 -1
- package/src/decode-loader-results.ts +7 -1
- package/src/escape-script.ts +52 -0
- package/src/handles/MetaTags.tsx +56 -5
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +29 -11
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +46 -0
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +7 -3
- package/src/host/pattern-matcher.ts +16 -2
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +5 -0
- package/src/response-utils.ts +25 -0
- package/src/route-definition/dsl-helpers.ts +7 -0
- package/src/route-definition/redirect.ts +1 -2
- package/src/router/content-negotiation.ts +58 -10
- package/src/router/intercept-resolution.ts +9 -0
- package/src/router/match-middleware/cache-store.ts +10 -1
- package/src/router/middleware.ts +10 -3
- package/src/router/pattern-matching.ts +25 -23
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +23 -0
- package/src/router/segment-resolution/fresh.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +35 -1
- package/src/router/segment-resolution/loader-cache.ts +10 -6
- package/src/router/segment-resolution/revalidation.ts +6 -0
- package/src/router/segment-resolution.ts +1 -0
- package/src/router/trie-matching.ts +14 -9
- package/src/router.ts +18 -10
- package/src/rsc/handler.ts +52 -13
- package/src/rsc/helpers.ts +7 -1
- package/src/rsc/index.ts +1 -4
- package/src/rsc/loader-fetch.ts +107 -37
- package/src/rsc/progressive-enhancement.ts +18 -6
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-route-handler.ts +16 -133
- package/src/rsc/rsc-rendering.ts +13 -4
- package/src/rsc/server-action.ts +52 -6
- package/src/rsc/types.ts +7 -0
- package/src/search-params.ts +24 -5
- package/src/segment-loader-promise.ts +17 -2
- package/src/server/loader-registry.ts +16 -18
- package/src/server/request-context.ts +47 -20
- package/src/testing/dispatch.ts +108 -25
- package/src/testing/flight.ts +25 -0
- package/src/testing/internal/context.ts +25 -2
- package/src/testing/render-handler.ts +3 -1
- package/src/testing/render-route.tsx +15 -0
- package/src/testing/run-loader.ts +10 -3
- package/src/theme/ThemeProvider.tsx +20 -6
- package/src/theme/ThemeScript.tsx +7 -3
- package/src/theme/constants.ts +54 -3
- package/src/theme/theme-script.ts +22 -7
- package/src/types/request-scope.ts +8 -3
- package/src/vite/plugins/cjs-to-esm.ts +8 -1
- package/src/vite/plugins/expose-id-utils.ts +10 -1
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
- package/src/vite/plugins/expose-internal-ids.ts +0 -1
- package/src/vite/plugins/version-plugin.ts +5 -17
- package/src/vite/plugins/virtual-entries.ts +12 -2
- package/src/vite/rango.ts +15 -6
- package/src/vite/utils/ast-handler-extract.ts +11 -4
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/prerender-utils.ts +17 -2
package/AGENTS.md
CHANGED
|
@@ -7,3 +7,11 @@ Run `/rango` to understand the API. Detailed guides for each feature are in the
|
|
|
7
7
|
## Development rules
|
|
8
8
|
|
|
9
9
|
- Always commit generated files (e.g. `*.gen.ts`) alongside the source changes that produced them.
|
|
10
|
+
|
|
11
|
+
## Repo-wide rules (read before pushing)
|
|
12
|
+
|
|
13
|
+
This package inherits the repo-wide conventions in the root [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md). The ones a package-scoped reader is most likely to miss:
|
|
14
|
+
|
|
15
|
+
- **Pre-push gate** — before EVERY push, run all of the following from the **repo root** and fix any failures: `pnpm run typecheck`, `pnpm run test:unit:all`, `pnpm run lint`, `pnpm run format`.
|
|
16
|
+
- **`test:unit:all` is recursive** — it runs the unit AND Flight/RSC suites for every package and consumer app (cloudflare-basic, mini, vite-rsc-demo, ...), not just `@rangojs/router`. A change can pass this package's own tests while breaking a consumer app's `@rangojs/router/testing` dogfood suite, so do not run only `pnpm --filter @rangojs/router test:unit`.
|
|
17
|
+
- **Dev + prod e2e parity is mandatory** — every e2e test must cover BOTH dev and production modes; never add a dev-only test without its production counterpart. See the dev/prod bucketing convention in the root `AGENTS.md`.
|
package/README.md
CHANGED
|
@@ -127,20 +127,29 @@ export const router = createRouter().routes(urlpatterns);
|
|
|
127
127
|
"use client";
|
|
128
128
|
|
|
129
129
|
import type { ReactNode } from "react";
|
|
130
|
-
import { MetaTags } from "@rangojs/router/client";
|
|
130
|
+
import { MetaTags, Scripts } from "@rangojs/router/client";
|
|
131
131
|
|
|
132
132
|
export function Document({ children }: { children: ReactNode }) {
|
|
133
133
|
return (
|
|
134
134
|
<html lang="en">
|
|
135
135
|
<head>
|
|
136
136
|
<MetaTags />
|
|
137
|
+
<Scripts />
|
|
137
138
|
</head>
|
|
138
|
-
<body>
|
|
139
|
+
<body>
|
|
140
|
+
<Scripts position="body" />
|
|
141
|
+
{children}
|
|
142
|
+
</body>
|
|
139
143
|
</html>
|
|
140
144
|
);
|
|
141
145
|
}
|
|
142
146
|
```
|
|
143
147
|
|
|
148
|
+
`<MetaTags />` and `<Scripts />` render the tags collected by the built-in `Meta`
|
|
149
|
+
and `Script` handles (see [Meta Tags](#meta-tags) and [Scripts](#scripts)). The
|
|
150
|
+
built-in `DefaultDocument` already includes all three sites, so this is only
|
|
151
|
+
needed for a custom document.
|
|
152
|
+
|
|
144
153
|
## Defining Routes
|
|
145
154
|
|
|
146
155
|
Rango is a named-route router first.
|
|
@@ -978,6 +987,38 @@ export function BlogPostPage(ctx: HandlerContext) {
|
|
|
978
987
|
|
|
979
988
|
Render collected tags in the document with `<MetaTags />` from `@rangojs/router/client`.
|
|
980
989
|
|
|
990
|
+
## Scripts
|
|
991
|
+
|
|
992
|
+
Inject `<script>` tags (analytics, GTM, widgets) the same way, using the built-in
|
|
993
|
+
`Script` handle — push a config from a handler, render with `<Scripts />`:
|
|
994
|
+
|
|
995
|
+
```tsx
|
|
996
|
+
import { Script } from "@rangojs/router";
|
|
997
|
+
import type { HandlerContext } from "@rangojs/router";
|
|
998
|
+
import { Outlet } from "@rangojs/router/client";
|
|
999
|
+
|
|
1000
|
+
export function RootLayout(ctx: HandlerContext) {
|
|
1001
|
+
// Inline bootstrap (GTM/GA4/Segment) — rendered with the request CSP nonce.
|
|
1002
|
+
ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
1003
|
+
// External async resource (loads on first encounter, deduped by src).
|
|
1004
|
+
ctx.use(Script)({
|
|
1005
|
+
id: "plausible",
|
|
1006
|
+
src: "https://plausible.io/js/script.js",
|
|
1007
|
+
async: true,
|
|
1008
|
+
attributes: { "data-domain": "example.com" },
|
|
1009
|
+
});
|
|
1010
|
+
return <Outlet />;
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
Render with `<Scripts />` (head) and `<Scripts position="body" />` (body) from
|
|
1015
|
+
`@rangojs/router/client` (both are wired in `DefaultDocument`). The request CSP
|
|
1016
|
+
nonce is applied automatically to document-rendered scripts. `ScriptConfig` is a
|
|
1017
|
+
discriminated union (inline / external-async / external-ordered), and inline +
|
|
1018
|
+
ordered scripts are document-load while async externals are React resources — see
|
|
1019
|
+
the [`/scripts` skill](./skills/scripts/SKILL.md) for the full execution contract
|
|
1020
|
+
and CSP guidance.
|
|
1021
|
+
|
|
981
1022
|
## CLI: `rango generate`
|
|
982
1023
|
|
|
983
1024
|
Route types are generated automatically by the Vite plugin. The CLI is a manual fallback for generating types outside the dev server (e.g. in CI or for IDE support before first `pnpm dev`):
|
package/dist/bin/rango.js
CHANGED
|
@@ -339,7 +339,7 @@ function extractIncludesWithDiagnostics(code, sourceFileArg) {
|
|
|
339
339
|
return { resolved, unresolvable };
|
|
340
340
|
}
|
|
341
341
|
function resolveImportedVariable(code, localName) {
|
|
342
|
-
const importRegex = /import\s
|
|
342
|
+
const importRegex = /import\s*(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
343
343
|
let match;
|
|
344
344
|
while ((match = importRegex.exec(code)) !== null) {
|
|
345
345
|
const imports = match[1];
|
|
@@ -635,25 +635,39 @@ function isLineTerminator(ch) {
|
|
|
635
635
|
const c = ch.charCodeAt(0);
|
|
636
636
|
return c === 10 || c === 13 || c === 8232 || c === 8233;
|
|
637
637
|
}
|
|
638
|
+
function isRegexPositionAt(code, slashPos, prevChar) {
|
|
639
|
+
if (prevChar === void 0) return true;
|
|
640
|
+
if (prevChar === ")" || prevChar === "]" || prevChar === "}") return false;
|
|
641
|
+
if (!/[\w$]/.test(prevChar)) return true;
|
|
642
|
+
let k = slashPos - 1;
|
|
643
|
+
while (k >= 0 && /\s/.test(code[k])) k--;
|
|
644
|
+
const wordEnd = k + 1;
|
|
645
|
+
while (k >= 0 && /[\w$]/.test(code[k])) k--;
|
|
646
|
+
return REGEX_PRECEDING_KEYWORDS.has(code.slice(k + 1, wordEnd));
|
|
647
|
+
}
|
|
638
648
|
function makeCodeClassifier(code) {
|
|
639
649
|
const n = code.length;
|
|
640
650
|
let i = 0;
|
|
641
651
|
let skipStart = -1;
|
|
642
652
|
let skipEnd = -1;
|
|
653
|
+
let lastSig;
|
|
643
654
|
return (q) => {
|
|
644
655
|
if (q >= skipStart && q < skipEnd) return false;
|
|
645
656
|
while (i < n && i <= q) {
|
|
646
657
|
const c = code[i];
|
|
647
658
|
const d = i + 1 < n ? code[i + 1] : "";
|
|
648
659
|
let end = -1;
|
|
660
|
+
let transparent = false;
|
|
649
661
|
if (c === "/" && d === "/") {
|
|
650
662
|
let j = i + 2;
|
|
651
663
|
while (j < n && !isLineTerminator(code[j])) j++;
|
|
652
664
|
end = j;
|
|
665
|
+
transparent = true;
|
|
653
666
|
} else if (c === "/" && d === "*") {
|
|
654
667
|
let j = i + 2;
|
|
655
668
|
while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
|
|
656
669
|
end = Math.min(n, j + 2);
|
|
670
|
+
transparent = true;
|
|
657
671
|
} else if (c === '"' || c === "'" || c === "`") {
|
|
658
672
|
let j = i + 1;
|
|
659
673
|
while (j < n) {
|
|
@@ -668,6 +682,29 @@ function makeCodeClassifier(code) {
|
|
|
668
682
|
j++;
|
|
669
683
|
}
|
|
670
684
|
end = j;
|
|
685
|
+
} else if (c === "/" && d !== "/" && d !== "*" && isRegexPositionAt(code, i, lastSig)) {
|
|
686
|
+
let j = i + 1;
|
|
687
|
+
let inClass = false;
|
|
688
|
+
let closed = false;
|
|
689
|
+
while (j < n && !isLineTerminator(code[j])) {
|
|
690
|
+
const r = code[j];
|
|
691
|
+
if (r === "\\") {
|
|
692
|
+
j += 2;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (r === "[") inClass = true;
|
|
696
|
+
else if (r === "]") inClass = false;
|
|
697
|
+
else if (r === "/" && !inClass) {
|
|
698
|
+
j++;
|
|
699
|
+
closed = true;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
j++;
|
|
703
|
+
}
|
|
704
|
+
if (closed) {
|
|
705
|
+
while (j < n && /[a-z]/.test(code[j])) j++;
|
|
706
|
+
end = j;
|
|
707
|
+
}
|
|
671
708
|
}
|
|
672
709
|
if (end >= 0) {
|
|
673
710
|
if (q < end) {
|
|
@@ -676,7 +713,9 @@ function makeCodeClassifier(code) {
|
|
|
676
713
|
return false;
|
|
677
714
|
}
|
|
678
715
|
i = end;
|
|
716
|
+
if (!transparent) lastSig = "x";
|
|
679
717
|
} else {
|
|
718
|
+
if (!/\s/.test(c)) lastSig = c;
|
|
680
719
|
i++;
|
|
681
720
|
}
|
|
682
721
|
}
|
|
@@ -693,9 +732,26 @@ function firstCodeMatchIndex(code, pattern) {
|
|
|
693
732
|
}
|
|
694
733
|
return -1;
|
|
695
734
|
}
|
|
735
|
+
var REGEX_PRECEDING_KEYWORDS;
|
|
696
736
|
var init_source_scan = __esm({
|
|
697
737
|
"src/build/route-types/source-scan.ts"() {
|
|
698
738
|
"use strict";
|
|
739
|
+
REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
|
|
740
|
+
"return",
|
|
741
|
+
"typeof",
|
|
742
|
+
"instanceof",
|
|
743
|
+
"in",
|
|
744
|
+
"of",
|
|
745
|
+
"new",
|
|
746
|
+
"delete",
|
|
747
|
+
"void",
|
|
748
|
+
"do",
|
|
749
|
+
"else",
|
|
750
|
+
"yield",
|
|
751
|
+
"await",
|
|
752
|
+
"case",
|
|
753
|
+
"throw"
|
|
754
|
+
]);
|
|
699
755
|
}
|
|
700
756
|
});
|
|
701
757
|
|
|
@@ -1153,11 +1209,16 @@ async function initializeApp() {
|
|
|
1153
1209
|
createTemporaryReferenceSet,
|
|
1154
1210
|
};
|
|
1155
1211
|
|
|
1156
|
-
|
|
1212
|
+
// initBrowserApp resolves the initial payload and returns the browser app
|
|
1213
|
+
// context, including strictMode (default true) from createRouter. StrictMode
|
|
1214
|
+
// is the default; createRouter({ strictMode: false }) ships the opt-out in the
|
|
1215
|
+
// payload metadata. StrictMode emits no DOM, so toggling never changes markup.
|
|
1216
|
+
const { strictMode } = await initBrowserApp({ rscStream, deps });
|
|
1157
1217
|
|
|
1218
|
+
const app = createElement(Rango);
|
|
1158
1219
|
hydrateRoot(
|
|
1159
1220
|
document,
|
|
1160
|
-
createElement(StrictMode, null,
|
|
1221
|
+
strictMode === false ? app : createElement(StrictMode, null, app)
|
|
1161
1222
|
);
|
|
1162
1223
|
}
|
|
1163
1224
|
|
|
@@ -1186,13 +1247,37 @@ export const renderHTML = createSSRHandler({
|
|
|
1186
1247
|
}
|
|
1187
1248
|
});
|
|
1188
1249
|
|
|
1250
|
+
// src/vite/utils/directive-prologue.ts
|
|
1251
|
+
import { parseAst } from "vite";
|
|
1252
|
+
function hasUseClientDirective(source) {
|
|
1253
|
+
let program;
|
|
1254
|
+
try {
|
|
1255
|
+
program = parseAst(source, { lang: "tsx" });
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
for (const node of program.body ?? []) {
|
|
1260
|
+
if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
|
|
1261
|
+
if (node.expression.value === "use client") return true;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
var init_directive_prologue = __esm({
|
|
1269
|
+
"src/vite/utils/directive-prologue.ts"() {
|
|
1270
|
+
"use strict";
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1189
1274
|
// src/vite/plugins/version-plugin.ts
|
|
1190
1275
|
var version_plugin_exports = {};
|
|
1191
1276
|
__export(version_plugin_exports, {
|
|
1192
1277
|
createVersionPlugin: () => createVersionPlugin,
|
|
1193
1278
|
isViteDepCachePath: () => isViteDepCachePath
|
|
1194
1279
|
});
|
|
1195
|
-
import { parseAst } from "vite";
|
|
1280
|
+
import { parseAst as parseAst2 } from "vite";
|
|
1196
1281
|
function isCodeModule(id) {
|
|
1197
1282
|
return /\.(tsx?|jsx?)($|\?)/.test(id);
|
|
1198
1283
|
}
|
|
@@ -1200,23 +1285,13 @@ function normalizeModuleId(id) {
|
|
|
1200
1285
|
return id.split("?", 1)[0];
|
|
1201
1286
|
}
|
|
1202
1287
|
function getClientModuleSignature(source) {
|
|
1288
|
+
if (!hasUseClientDirective(source)) return void 0;
|
|
1203
1289
|
let program;
|
|
1204
1290
|
try {
|
|
1205
|
-
program =
|
|
1291
|
+
program = parseAst2(source, { lang: "tsx" });
|
|
1206
1292
|
} catch {
|
|
1207
1293
|
return void 0;
|
|
1208
1294
|
}
|
|
1209
|
-
let isUseClient = false;
|
|
1210
|
-
for (const node of program.body ?? []) {
|
|
1211
|
-
if (node?.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string") {
|
|
1212
|
-
if (node.expression.value === "use client") {
|
|
1213
|
-
isUseClient = true;
|
|
1214
|
-
}
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1217
|
-
break;
|
|
1218
|
-
}
|
|
1219
|
-
if (!isUseClient) return void 0;
|
|
1220
1295
|
const exports = /* @__PURE__ */ new Set();
|
|
1221
1296
|
let hasDefault = false;
|
|
1222
1297
|
let hasExportAll = false;
|
|
@@ -1390,6 +1465,7 @@ var init_version_plugin = __esm({
|
|
|
1390
1465
|
"src/vite/plugins/version-plugin.ts"() {
|
|
1391
1466
|
"use strict";
|
|
1392
1467
|
init_virtual_entries();
|
|
1468
|
+
init_directive_prologue();
|
|
1393
1469
|
}
|
|
1394
1470
|
});
|
|
1395
1471
|
|