@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109
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/README.md +4 -4
- package/dist/bin/rango.js +16 -16
- package/dist/vite/index.js +146 -150
- package/package.json +6 -6
- package/skills/hooks/SKILL.md +2 -0
- package/skills/links/SKILL.md +13 -1
- package/skills/loader/SKILL.md +1 -1
- package/skills/middleware/SKILL.md +3 -3
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/prerender/SKILL.md +13 -13
- package/skills/rango/SKILL.md +9 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/typesafety/SKILL.md +273 -31
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/react/location-state-shared.ts +3 -3
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/rsc-router.tsx +14 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/build/generate-manifest.ts +3 -3
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +3 -3
- package/src/build/route-types/router-processing.ts +4 -4
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/client.tsx +4 -7
- package/src/errors.ts +1 -1
- package/src/handle.ts +2 -2
- package/src/href-client.ts +136 -19
- package/src/index.rsc.ts +4 -4
- package/src/index.ts +2 -2
- package/src/loader.rsc.ts +1 -1
- package/src/loader.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/route-definition/dsl-helpers.ts +2 -2
- package/src/route-definition/helpers-types.ts +2 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/prerender-match.ts +1 -1
- package/src/router/router-interfaces.ts +34 -28
- package/src/router/router-options.ts +1 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/revalidation.ts +2 -2
- package/src/router.ts +13 -16
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +16 -16
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +3 -3
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/pattern-types.ts +34 -0
- package/src/urls/type-extraction.ts +6 -1
- package/src/use-loader.tsx +6 -4
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +3 -3
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +3 -3
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +33 -9
- package/src/vite/plugins/expose-internal-ids.ts +1 -1
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +1 -1
- package/src/vite/plugins/version-plugin.ts +2 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +11 -11
- package/src/vite/router-discovery.ts +26 -29
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/forward-user-plugins.ts +46 -17
- package/src/vite/utils/shared-utils.ts +26 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.109",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"scripts": {
|
|
136
136
|
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
137
|
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
|
|
138
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
139
139
|
"test": "playwright test",
|
|
140
140
|
"test:ui": "playwright test --ui",
|
|
141
141
|
"test:unit": "vitest run",
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
145
|
"@types/debug": "^4.1.12",
|
|
146
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
146
|
+
"@vitejs/plugin-rsc": "^0.5.26",
|
|
147
147
|
"debug": "^4.4.1",
|
|
148
148
|
"magic-string": "^0.30.17",
|
|
149
149
|
"picomatch": "^4.0.3",
|
|
@@ -163,11 +163,11 @@
|
|
|
163
163
|
"vitest": "^4.0.0"
|
|
164
164
|
},
|
|
165
165
|
"peerDependencies": {
|
|
166
|
-
"@cloudflare/vite-plugin": "^1.
|
|
167
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
166
|
+
"@cloudflare/vite-plugin": "^1.38.0",
|
|
167
|
+
"@vitejs/plugin-rsc": "^0.5.26",
|
|
168
168
|
"react": ">=19.2.6 <20",
|
|
169
169
|
"react-dom": ">=19.2.6 <20",
|
|
170
|
-
"vite": "^
|
|
170
|
+
"vite": "^8.0.0"
|
|
171
171
|
},
|
|
172
172
|
"peerDependenciesMeta": {
|
|
173
173
|
"@cloudflare/vite-plugin": {
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -776,6 +776,8 @@ function MountInfo() {
|
|
|
776
776
|
|
|
777
777
|
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse(".name", params?)`. Auto-fills params from `useParams()`; explicit params override.
|
|
778
778
|
|
|
779
|
+
> Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
|
|
780
|
+
|
|
779
781
|
```tsx
|
|
780
782
|
"use client";
|
|
781
783
|
import { Link, useReverse } from "@rangojs/router/client";
|
package/skills/links/SKILL.md
CHANGED
|
@@ -213,7 +213,17 @@ function GlobalNav() {
|
|
|
213
213
|
}
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
`href()` provides compile-time validation via `
|
|
216
|
+
`href()` provides compile-time validation via the `Rango.Path` type. Paths are validated against registered route patterns using `PatternToPath`.
|
|
217
|
+
|
|
218
|
+
When wrapping `href()`, type the wrapper's path parameter as `Rango.Path` so it
|
|
219
|
+
keeps the same generated-route validation. `Rango.Path` is ambient — no import,
|
|
220
|
+
just like `Rango.Env` / `Rango.Vars`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { href } from "@rangojs/router/client";
|
|
224
|
+
|
|
225
|
+
export const appHref = (path: Rango.Path): string => href(path);
|
|
226
|
+
```
|
|
217
227
|
|
|
218
228
|
`href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
|
|
219
229
|
|
|
@@ -263,6 +273,8 @@ function MountInfo() {
|
|
|
263
273
|
|
|
264
274
|
Hook that returns a typed local reverse function for a `routes` map imported from a generated `.gen.ts` next to a `urls()` module. The route map is the **exposure boundary** — `useReverse` only knows about names in that map, never the full app manifest.
|
|
265
275
|
|
|
276
|
+
> Import the per-module `routes` (e.g. `urls/blog.gen.ts`), **not** `router.named-routes.gen.ts`. The named-routes file is the whole app manifest and is server-only data — importing it into a client component pulls every route name into the client bundle.
|
|
277
|
+
|
|
266
278
|
```tsx
|
|
267
279
|
"use client";
|
|
268
280
|
import { Link, useReverse } from "@rangojs/router/client";
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -185,7 +185,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
185
185
|
// Request headers
|
|
186
186
|
const auth = ctx.request.headers.get("Authorization");
|
|
187
187
|
|
|
188
|
-
// Variables set by middleware (from
|
|
188
|
+
// Variables set by middleware (from Rango.Vars augmentation)
|
|
189
189
|
const user = ctx.get("user");
|
|
190
190
|
|
|
191
191
|
// Type-checked URLs for payloads. `.name` resolves within the current
|
|
@@ -196,7 +196,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
196
196
|
ctx.env.DB; // D1Database
|
|
197
197
|
ctx.env.KV; // KVNamespace
|
|
198
198
|
|
|
199
|
-
// Set variables for downstream handlers (typed via
|
|
199
|
+
// Set variables for downstream handlers (typed via Rango.Vars)
|
|
200
200
|
ctx.set("user", { id: "123", name: "John" });
|
|
201
201
|
|
|
202
202
|
// Continue to next middleware/handler
|
|
@@ -237,8 +237,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
|
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
|
|
240
|
-
via
|
|
241
|
-
data; use
|
|
240
|
+
via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
|
|
241
|
+
data; use Rango.Vars for app-wide middleware state.
|
|
242
242
|
|
|
243
243
|
## Redirect with State in Middleware
|
|
244
244
|
|
|
@@ -108,6 +108,33 @@ path.text("/api/data", () => "plain text version", { name: "dataText" }),
|
|
|
108
108
|
Without an RSC primary, there is no `text/html` candidate — the Accept header
|
|
109
109
|
picks among the response-type candidates directly.
|
|
110
110
|
|
|
111
|
+
## Type Safety For Negotiated Paths
|
|
112
|
+
|
|
113
|
+
`router.named-routes.gen.ts` validates route names, params, search, `href()`, and
|
|
114
|
+
the `Rango.Path` type, but it does not carry response payload metadata. For MIME or
|
|
115
|
+
response payload types, use one of these surfaces:
|
|
116
|
+
|
|
117
|
+
- `RouteResponse<typeof patterns, "routeName">` for a specific response variant
|
|
118
|
+
by route name. This is the clearest option when several MIME variants share
|
|
119
|
+
one URL pattern.
|
|
120
|
+
- `Rango.PathResponse<"/products/:id">` (ambient, no import) for global lookup by URL pattern or concrete path after the app
|
|
121
|
+
registers `typeof router.routeMap`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// router.tsx
|
|
125
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
126
|
+
|
|
127
|
+
declare global {
|
|
128
|
+
namespace Rango {
|
|
129
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`RegisteredRoutes` is what exposes the richer routeMap entries containing
|
|
135
|
+
response payload metadata. Without it, URL-pattern response lookup has paths but
|
|
136
|
+
no payloads, so response types resolve to `ResponseEnvelope<never>`.
|
|
137
|
+
|
|
111
138
|
## How It Works
|
|
112
139
|
|
|
113
140
|
1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
|
|
@@ -358,16 +358,16 @@ Both error types propagate to the router's `onError` callback with phase
|
|
|
358
358
|
The build produces per-URL timing logs:
|
|
359
359
|
|
|
360
360
|
```
|
|
361
|
-
[
|
|
362
|
-
[
|
|
363
|
-
[
|
|
364
|
-
[
|
|
365
|
-
[
|
|
366
|
-
|
|
367
|
-
[
|
|
368
|
-
[
|
|
369
|
-
[
|
|
370
|
-
[
|
|
361
|
+
[rango] Pre-rendering 12 URL(s) (concurrency: 4)...
|
|
362
|
+
[rango] OK /articles/hello (42ms)
|
|
363
|
+
[rango] PASS /articles/remote-only (5ms) - live fallback
|
|
364
|
+
[rango] SKIP /articles/draft-post (3ms) - Article is a draft
|
|
365
|
+
[rango] Pre-render complete: 11 done, 1 skipped (1204ms total)
|
|
366
|
+
|
|
367
|
+
[rango] Rendering 3 static handler(s)...
|
|
368
|
+
[rango] OK DocsLayout (28ms)
|
|
369
|
+
[rango] SKIP TocSidebar (1ms) - Not ready
|
|
370
|
+
[rango] Static render complete: 2 done, 1 skipped (120ms total)
|
|
371
371
|
```
|
|
372
372
|
|
|
373
373
|
A `FAIL` line is logged per-URL when a handler throws a non-Skip error. The
|
|
@@ -463,9 +463,9 @@ export const Product = Passthrough(ProductDef, async (ctx) => {
|
|
|
463
463
|
Passthrough entries are logged distinctly:
|
|
464
464
|
|
|
465
465
|
```
|
|
466
|
-
[
|
|
467
|
-
[
|
|
468
|
-
[
|
|
466
|
+
[rango] OK /blog/a (42ms)
|
|
467
|
+
[rango] PASS /blog/b (3ms) - live fallback
|
|
468
|
+
[rango] OK /blog/c (38ms)
|
|
469
469
|
```
|
|
470
470
|
|
|
471
471
|
## Edge Cases and Constraints
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -266,6 +266,15 @@ Each file is classified by its contents:
|
|
|
266
266
|
Directories are scanned recursively for `.ts`/`.tsx` files, skipping `node_modules`,
|
|
267
267
|
dotfiles, and existing `.gen.` files.
|
|
268
268
|
|
|
269
|
+
> The two generated files are **not interchangeable surfaces**.
|
|
270
|
+
> `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
|
|
271
|
+
> named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
|
|
272
|
+
> Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
|
|
273
|
+
> and explicit local handler typing (`Handler<".name", routes>`). Neither
|
|
274
|
+
> carries response payloads — response/MIME payload inference comes from
|
|
275
|
+
> `typeof router.routeMap` via `RegisteredRoutes`, not `*.named-routes.gen.ts`.
|
|
276
|
+
> See `/typesafety` for the full surface breakdown.
|
|
277
|
+
|
|
269
278
|
### Recursive includes
|
|
270
279
|
|
|
271
280
|
The generator follows `include()` calls across files, resolving imports to build
|
|
@@ -236,22 +236,69 @@ type ProductsData = RouteResponse<typeof apiPatterns, "products">;
|
|
|
236
236
|
// = ResponseEnvelope<{ id: string; name: string; price: number }[]>
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
### PathResponse (global lookup by URL pattern)
|
|
239
|
+
### Rango.PathResponse (global lookup by URL pattern or concrete path)
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
`Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
|
|
242
|
+
which carries response payload metadata. That surface is **not** auto-wired —
|
|
243
|
+
without the augmentation below, `Rango.PathResponse` falls back to the generated
|
|
244
|
+
path/search map, or to a permissive map when nothing is generated. Either way, it
|
|
245
|
+
has no response payload metadata, so response routes resolve to
|
|
246
|
+
`ResponseEnvelope<never>`:
|
|
242
247
|
|
|
243
248
|
```typescript
|
|
244
|
-
|
|
249
|
+
// router.tsx
|
|
250
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
245
251
|
|
|
252
|
+
declare global {
|
|
253
|
+
namespace Rango {
|
|
254
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
With that in place, look up the response type by URL pattern (ambient, no import):
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
246
262
|
// After include("/api", apiPatterns) in main urls
|
|
247
|
-
type Health = PathResponse<"/api/health">;
|
|
263
|
+
type Health = Rango.PathResponse<"/api/health">;
|
|
248
264
|
// = ResponseEnvelope<{ status: string; timestamp: number }>
|
|
249
265
|
|
|
250
266
|
// RSC routes return ResponseEnvelope<never>
|
|
251
|
-
type Home = PathResponse<"/">;
|
|
267
|
+
type Home = Rango.PathResponse<"/">;
|
|
252
268
|
// = ResponseEnvelope<never>
|
|
253
269
|
```
|
|
254
270
|
|
|
271
|
+
`Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
|
|
272
|
+
wrapper whose response is inferred from the path you pass:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { href } from "@rangojs/router/client";
|
|
276
|
+
|
|
277
|
+
async function get<T extends Rango.Path>(
|
|
278
|
+
path: T,
|
|
279
|
+
): Promise<Rango.PathResponse<T>> {
|
|
280
|
+
return fetch(href(path)).then((r) => r.json());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const product = await get("/api/products/42"); // ResponseEnvelope<Product>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
287
|
+
route can match several patterns and union their responses.
|
|
288
|
+
|
|
289
|
+
`Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
|
|
290
|
+
return: `path.json()` serializes with `JSON.stringify`, so a handler returning
|
|
291
|
+
`{ createdAt: Date }` resolves to `ResponseEnvelope<{ createdAt: string }>`. This
|
|
292
|
+
runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
293
|
+
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
|
|
294
|
+
`RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
|
|
295
|
+
both response lookups report the identical wire shape.
|
|
296
|
+
|
|
297
|
+
For local/scoped response typing without global augmentation, prefer
|
|
298
|
+
`RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
|
|
299
|
+
the response payload straight from the `urls()` patterns and needs no
|
|
300
|
+
`RegisteredRoutes` wiring.
|
|
301
|
+
|
|
255
302
|
### ParamsFor with Response Routes
|
|
256
303
|
|
|
257
304
|
```typescript
|
|
@@ -361,14 +408,16 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
361
408
|
|
|
362
409
|
```typescript
|
|
363
410
|
import type { RouteResponse } from "@rangojs/router";
|
|
364
|
-
import type {
|
|
411
|
+
import type { ParamsFor } from "@rangojs/router/client";
|
|
365
412
|
|
|
366
|
-
// Scoped (before mount) -- use the module directly
|
|
413
|
+
// Scoped (before mount) -- use the module directly, no global wiring needed
|
|
367
414
|
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
368
415
|
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
369
416
|
|
|
370
|
-
// After mounting -- names get prefixed
|
|
371
|
-
|
|
417
|
+
// After mounting -- names get prefixed.
|
|
418
|
+
// Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
|
|
419
|
+
// otherwise it resolves to ResponseEnvelope<never>.
|
|
420
|
+
type BlogStats = Rango.PathResponse<"/blog/api/stats">;
|
|
372
421
|
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
373
422
|
|
|
374
423
|
// Params work through nested includes
|
|
@@ -71,7 +71,7 @@ urls(
|
|
|
71
71
|
## Router Options
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
interface
|
|
74
|
+
interface RangoOptions<TEnv> {
|
|
75
75
|
// URL patterns from urls() function
|
|
76
76
|
urls: UrlPatterns;
|
|
77
77
|
|
|
@@ -405,7 +405,7 @@ interface AppBindings {
|
|
|
405
405
|
KV: KVNamespace;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// Variables declared via
|
|
408
|
+
// Variables declared via global namespace augmentation
|
|
409
409
|
interface AppVariables {
|
|
410
410
|
user?: { id: string; name: string };
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
|
|
|
417
417
|
|
|
418
418
|
// Register types globally for implicit typing
|
|
419
419
|
declare global {
|
|
420
|
-
namespace
|
|
420
|
+
namespace Rango {
|
|
421
421
|
interface Env extends AppBindings {}
|
|
422
422
|
interface Vars extends AppVariables {}
|
|
423
423
|
}
|