@nifrajs/web 2.1.0 → 2.3.0
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 +3 -3
- package/dist/build-vite.d.ts +68 -0
- package/dist/build-vite.d.ts.map +1 -0
- package/dist/build-vite.js +433 -0
- package/dist/build-vite.js.map +1 -0
- package/dist/build.d.ts +174 -65
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +376 -135
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +83 -0
- package/dist/bun-dev-entry.d.ts.map +1 -0
- package/dist/bun-dev-entry.js +136 -0
- package/dist/bun-dev-entry.js.map +1 -0
- package/dist/chunk-names.js +1 -1
- package/dist/client.d.ts +3 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +209 -27
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +2 -2
- package/dist/conformance.js +3 -3
- package/dist/deferred.d.ts +11 -11
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +16 -16
- package/dist/dev-error.d.ts +2 -2
- package/dist/dev-error.js +3 -3
- package/dist/dev-port.d.ts +36 -0
- package/dist/dev-port.d.ts.map +1 -0
- package/dist/dev-port.js +48 -0
- package/dist/dev-port.js.map +1 -0
- package/dist/dev.d.ts +62 -5
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +419 -60
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +44 -7
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +92 -9
- package/dist/draft.js.map +1 -1
- package/dist/fn-state.d.ts +56 -0
- package/dist/fn-state.d.ts.map +1 -0
- package/dist/fn-state.js +77 -0
- package/dist/fn-state.js.map +1 -0
- package/dist/fn.d.ts +111 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +125 -0
- package/dist/fn.js.map +1 -0
- package/dist/fonts-google.d.ts +9 -9
- package/dist/fonts-google.js +10 -10
- package/dist/fonts.d.ts +7 -7
- package/dist/fonts.js +1 -1
- package/dist/forms.d.ts +8 -8
- package/dist/fs.d.ts +15 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +8 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +266 -74
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +869 -179
- package/dist/index.js.map +1 -1
- package/dist/internal/head-attributes.d.ts +15 -0
- package/dist/internal/head-attributes.d.ts.map +1 -0
- package/dist/internal/head-attributes.js +99 -0
- package/dist/internal/head-attributes.js.map +1 -0
- package/dist/internal/script-types.d.ts +14 -0
- package/dist/internal/script-types.d.ts.map +1 -0
- package/dist/internal/script-types.js +17 -0
- package/dist/internal/script-types.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +60 -0
- package/dist/internal/server-boundary.d.ts.map +1 -0
- package/dist/internal/server-boundary.js +174 -0
- package/dist/internal/server-boundary.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +16 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -0
- package/dist/internal/timing-safe-equal.js +26 -0
- package/dist/internal/timing-safe-equal.js.map +1 -0
- package/dist/internal/vite-import.d.ts +63 -0
- package/dist/internal/vite-import.d.ts.map +1 -0
- package/dist/internal/vite-import.js +94 -0
- package/dist/internal/vite-import.js.map +1 -0
- package/dist/islands.d.ts +5 -5
- package/dist/islands.js +5 -5
- package/dist/isr.d.ts +15 -15
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +8 -21
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +12 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +121 -40
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +136 -26
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +91 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/module-graph.js +92 -0
- package/dist/module-graph.js.map +1 -0
- package/dist/navigation.d.ts +120 -5
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +37 -5
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +3 -3
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +8 -8
- package/dist/plugins/kit.js +9 -9
- package/dist/plugins/scss.js +3 -3
- package/dist/plugins/vite-leak-guard.d.ts +45 -0
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -0
- package/dist/plugins/vite-leak-guard.js +67 -0
- package/dist/plugins/vite-leak-guard.js.map +1 -0
- package/dist/plugins/vite-server-fn.d.ts +23 -0
- package/dist/plugins/vite-server-fn.d.ts.map +1 -0
- package/dist/plugins/vite-server-fn.js +51 -0
- package/dist/plugins/vite-server-fn.js.map +1 -0
- package/dist/plugins/vite-server-only.d.ts +49 -0
- package/dist/plugins/vite-server-only.d.ts.map +1 -0
- package/dist/plugins/vite-server-only.js +57 -0
- package/dist/plugins/vite-server-only.js.map +1 -0
- package/dist/prerender.d.ts +5 -5
- package/dist/prerender.js +7 -7
- package/dist/public-dir.d.ts +36 -0
- package/dist/public-dir.d.ts.map +1 -0
- package/dist/public-dir.js +89 -0
- package/dist/public-dir.js.map +1 -0
- package/dist/query.d.ts +10 -10
- package/dist/query.js +15 -15
- package/dist/route-manifest.d.ts +91 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +128 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/router.d.ts +94 -22
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +170 -58
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts +94 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +232 -0
- package/dist/search.js.map +1 -0
- package/dist/server-only.d.ts +6 -6
- package/dist/server-only.js +7 -7
- package/dist/service-worker.d.ts +56 -0
- package/dist/service-worker.d.ts.map +1 -0
- package/dist/service-worker.js +127 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/vite.d.ts +25 -17
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +75 -33
- package/dist/vite.js.map +1 -1
- package/package.json +48 -4
- package/server-manifest.d.ts +31 -0
- package/src/build-vite.ts +553 -0
- package/src/build.ts +563 -172
- package/src/bun-dev-entry.ts +184 -0
- package/src/chunk-names.ts +1 -1
- package/src/client.ts +221 -30
- package/src/conformance.ts +3 -3
- package/src/deferred.ts +17 -17
- package/src/dev-error.ts +3 -3
- package/src/dev-port.ts +63 -0
- package/src/dev.ts +468 -66
- package/src/draft.ts +124 -12
- package/src/fn-state.ts +107 -0
- package/src/fn.ts +199 -0
- package/src/fonts-google.ts +13 -13
- package/src/fonts.ts +7 -7
- package/src/forms.ts +8 -8
- package/src/fs.ts +24 -6
- package/src/index.ts +1174 -225
- package/src/internal/head-attributes.ts +109 -0
- package/src/internal/script-types.ts +18 -0
- package/src/internal/server-boundary.ts +187 -0
- package/src/internal/timing-safe-equal.ts +24 -0
- package/src/internal/vite-import.ts +92 -0
- package/src/islands.ts +7 -7
- package/src/isr.ts +18 -30
- package/src/llms-txt.ts +13 -5
- package/src/manifest.ts +264 -48
- package/src/module-graph.ts +146 -0
- package/src/navigation.ts +151 -5
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +11 -11
- package/src/plugins/scss.ts +3 -3
- package/src/plugins/vite-leak-guard.ts +110 -0
- package/src/plugins/vite-server-fn.ts +67 -0
- package/src/plugins/vite-server-only.ts +75 -0
- package/src/prerender.ts +10 -10
- package/src/public-dir.ts +108 -0
- package/src/query.ts +22 -22
- package/src/route-manifest.ts +240 -0
- package/src/router.ts +288 -74
- package/src/search.ts +297 -0
- package/src/server-only.ts +7 -7
- package/src/service-worker.ts +165 -0
- package/src/vite.ts +103 -49
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/route-manifest` - what each route actually DOES, as one artifact.
|
|
3
|
+
*
|
|
4
|
+
* The facts are already in the route modules: `prerender`, `getStaticPaths`, `revalidate`, `hydrate`.
|
|
5
|
+
* What has never existed is anywhere to read them together. To answer "which pages are static?", "which
|
|
6
|
+
* ones revalidate, and how often?", "which ship no JS?", you open every route file and hold the answer in
|
|
7
|
+
* your head - and the answer changes on the target you deploy to, which is nowhere near the route file.
|
|
8
|
+
*
|
|
9
|
+
* That gap is not just inconvenient. The interesting cases are the ones where a route's declaration and
|
|
10
|
+
* its deploy target disagree, and there is currently nothing that can notice: an ISR route on a target
|
|
11
|
+
* with no revalidation story regenerates on every request; a `static` build containing a route that was
|
|
12
|
+
* never prerenderable ships a page that 404s. Both are silent, and both are decided at build time by
|
|
13
|
+
* information no single place holds.
|
|
14
|
+
*
|
|
15
|
+
* So this derives one record per route - render mode, hydration, cache policy - resolves it against the
|
|
16
|
+
* target, and reports what the target cannot honour. Pure and fs-free: it takes an already-built
|
|
17
|
+
* `Manifest` so it runs at build time, in `nifra routes`, and in a test with a hand-built manifest.
|
|
18
|
+
*/
|
|
19
|
+
import type { Manifest, RouteModule } from "./manifest.ts"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How a route produces its HTML.
|
|
23
|
+
*
|
|
24
|
+
* - `static` - prerendered at build; served as a file, no server render at request time.
|
|
25
|
+
* - `isr` - server-rendered, then cached and revalidated on a timer (`export const revalidate`).
|
|
26
|
+
* - `ssr` - server-rendered per request. The default, and the only mode that needs a live server.
|
|
27
|
+
*/
|
|
28
|
+
export type RenderMode = "static" | "isr" | "ssr"
|
|
29
|
+
|
|
30
|
+
/** What a route needs from its host in order to behave as declared. */
|
|
31
|
+
export type RouteCapability = "server" | "revalidation"
|
|
32
|
+
|
|
33
|
+
/** One route's resolved behaviour. */
|
|
34
|
+
export interface RouteManifestEntry {
|
|
35
|
+
/** The route's id (its key in the build manifest and in `routes`). */
|
|
36
|
+
readonly id: string
|
|
37
|
+
/** The matching pattern, e.g. `/blog/:slug`. */
|
|
38
|
+
readonly pattern: string
|
|
39
|
+
readonly mode: RenderMode
|
|
40
|
+
/** Whether the full-document client is shipped. `false` means the page loads no framework JS. */
|
|
41
|
+
readonly hydrate: boolean
|
|
42
|
+
/** Revalidation window in seconds. Present only for `isr`. */
|
|
43
|
+
readonly revalidate?: number
|
|
44
|
+
/**
|
|
45
|
+
* Concrete paths prerendered at build. Present for `static` routes: a single path for a static route,
|
|
46
|
+
* one per `getStaticPaths` entry for a dynamic one. Absent means "no path was enumerated", which for a
|
|
47
|
+
* dynamic route is exactly the condition that makes it unprerenderable.
|
|
48
|
+
*/
|
|
49
|
+
readonly prerenderedPaths?: readonly string[]
|
|
50
|
+
/** What this route needs from the host to behave as declared. */
|
|
51
|
+
readonly requires: readonly RouteCapability[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A route whose declaration the chosen target cannot honour, and what actually happens if it ships. */
|
|
55
|
+
export interface RouteManifestConflict {
|
|
56
|
+
readonly id: string
|
|
57
|
+
readonly pattern: string
|
|
58
|
+
readonly capability: RouteCapability
|
|
59
|
+
/** What goes wrong on this target - the consequence, not the rule that was broken. */
|
|
60
|
+
readonly consequence: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The whole artifact: every route's behaviour, plus anything the target cannot honour. */
|
|
64
|
+
export interface RouteManifest {
|
|
65
|
+
/** The deploy target this was resolved against, when one was given. */
|
|
66
|
+
readonly target?: string
|
|
67
|
+
readonly routes: readonly RouteManifestEntry[]
|
|
68
|
+
readonly conflicts: readonly RouteManifestConflict[]
|
|
69
|
+
/** Route counts by mode - the summary a report leads with. */
|
|
70
|
+
readonly totals: Readonly<Record<RenderMode, number>>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A route is dynamic when its pattern carries a param or wildcard segment. */
|
|
74
|
+
const isDynamic = (pattern: string): boolean => /[:*]/.test(pattern)
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Derive one route's behaviour from its module exports.
|
|
78
|
+
*
|
|
79
|
+
* `prerender`/`getStaticPaths` win over `revalidate`: a page rendered at build time is not revalidated at
|
|
80
|
+
* runtime, so if a route declares both, the build-time answer is the one that describes what ships. That
|
|
81
|
+
* combination is worth surfacing rather than silently resolving, which is what `conflicts` is for once a
|
|
82
|
+
* target is known.
|
|
83
|
+
*/
|
|
84
|
+
export function deriveRouteEntry(
|
|
85
|
+
id: string,
|
|
86
|
+
pattern: string,
|
|
87
|
+
module: Pick<RouteModule, "prerender" | "getStaticPaths" | "revalidate" | "hydrate">,
|
|
88
|
+
prerenderedPaths?: readonly string[],
|
|
89
|
+
): RouteManifestEntry {
|
|
90
|
+
const wantsPrerender = module.prerender === true || module.getStaticPaths !== undefined
|
|
91
|
+
// A dynamic route is only really static if concrete paths were enumerated for it. `getStaticPaths`
|
|
92
|
+
// declares the intent; the paths are the evidence, and without them there is nothing prerendered to
|
|
93
|
+
// serve. Treating intent as sufficient is how a "static" build ships a page that 404s.
|
|
94
|
+
const enumerated = prerenderedPaths !== undefined && prerenderedPaths.length > 0
|
|
95
|
+
const isStatic = wantsPrerender && (!isDynamic(pattern) || enumerated)
|
|
96
|
+
const mode: RenderMode = isStatic ? "static" : module.revalidate !== undefined ? "isr" : "ssr"
|
|
97
|
+
|
|
98
|
+
const requires: RouteCapability[] = []
|
|
99
|
+
if (mode !== "static") requires.push("server")
|
|
100
|
+
if (mode === "isr") requires.push("revalidation")
|
|
101
|
+
|
|
102
|
+
const entry: {
|
|
103
|
+
id: string
|
|
104
|
+
pattern: string
|
|
105
|
+
mode: RenderMode
|
|
106
|
+
hydrate: boolean
|
|
107
|
+
revalidate?: number
|
|
108
|
+
prerenderedPaths?: readonly string[]
|
|
109
|
+
requires: readonly RouteCapability[]
|
|
110
|
+
} = { id, pattern, mode, hydrate: module.hydrate !== false, requires }
|
|
111
|
+
if (mode === "isr" && module.revalidate !== undefined) entry.revalidate = module.revalidate
|
|
112
|
+
if (prerenderedPaths !== undefined) entry.prerenderedPaths = prerenderedPaths
|
|
113
|
+
return entry as RouteManifestEntry
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* What each deploy target can actually do.
|
|
118
|
+
*
|
|
119
|
+
* `static` is the one that matters: it has no server at all, so every non-prerendered route is a page
|
|
120
|
+
* that will 404 in production while working perfectly in dev. The rest all run a server; they differ in
|
|
121
|
+
* how revalidation is wired, not in whether it exists, so this deliberately does not invent finer
|
|
122
|
+
* distinctions it cannot verify.
|
|
123
|
+
*/
|
|
124
|
+
const TARGET_CAPABILITIES: Readonly<Record<string, readonly RouteCapability[]>> = {
|
|
125
|
+
bun: ["server"],
|
|
126
|
+
node: ["server"],
|
|
127
|
+
deno: ["server"],
|
|
128
|
+
"cf-pages": ["server"],
|
|
129
|
+
vercel: ["server"],
|
|
130
|
+
static: [],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const CONSEQUENCE: Readonly<Record<RouteCapability, string>> = {
|
|
134
|
+
server:
|
|
135
|
+
"this target ships no server, so the route is never rendered - the URL 404s in production while working in dev. Add `export const prerender = true` (or `getStaticPaths` for a dynamic route), or build for a target that runs a server.",
|
|
136
|
+
revalidation:
|
|
137
|
+
"this target cannot revalidate a cached page, so `revalidate` has no effect and the route renders on every request.",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the route manifest for a discovered app, optionally resolved against a deploy target.
|
|
142
|
+
*
|
|
143
|
+
* `prerendered` maps a route id to the concrete paths the build actually emitted for it - pass what
|
|
144
|
+
* `prerenderRoutes` produced. Without it, dynamic routes are reported by their declaration alone, which
|
|
145
|
+
* is the right answer before a build has run and the wrong one after.
|
|
146
|
+
*/
|
|
147
|
+
export async function buildRouteManifest(
|
|
148
|
+
manifest: Manifest,
|
|
149
|
+
options: {
|
|
150
|
+
readonly target?: string
|
|
151
|
+
readonly prerendered?: Readonly<Record<string, readonly string[]>>
|
|
152
|
+
/** Runtime capabilities supplied outside generated entries (for example an explicit `withISR`). */
|
|
153
|
+
readonly capabilities?: readonly RouteCapability[]
|
|
154
|
+
} = {},
|
|
155
|
+
): Promise<RouteManifest> {
|
|
156
|
+
const entries: RouteManifestEntry[] = []
|
|
157
|
+
for (const route of manifest.routes) {
|
|
158
|
+
// Loading the module is what makes this authoritative rather than a guess from the filename: the
|
|
159
|
+
// render mode lives in the module's exports and nowhere else.
|
|
160
|
+
const module = (await route.load()) as RouteModule
|
|
161
|
+
entries.push(deriveRouteEntry(route.id, route.pattern, module, options.prerendered?.[route.id]))
|
|
162
|
+
}
|
|
163
|
+
entries.sort((a, b) => a.pattern.localeCompare(b.pattern))
|
|
164
|
+
|
|
165
|
+
const conflicts: RouteManifestConflict[] = []
|
|
166
|
+
const capabilities =
|
|
167
|
+
options.capabilities ??
|
|
168
|
+
(options.target === undefined ? undefined : TARGET_CAPABILITIES[options.target])
|
|
169
|
+
if (capabilities !== undefined) {
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
for (const capability of entry.requires) {
|
|
172
|
+
if (capabilities.includes(capability)) continue
|
|
173
|
+
conflicts.push({
|
|
174
|
+
id: entry.id,
|
|
175
|
+
pattern: entry.pattern,
|
|
176
|
+
capability,
|
|
177
|
+
consequence: CONSEQUENCE[capability],
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const totals: Record<RenderMode, number> = { static: 0, isr: 0, ssr: 0 }
|
|
184
|
+
for (const entry of entries) totals[entry.mode] += 1
|
|
185
|
+
|
|
186
|
+
const result: {
|
|
187
|
+
target?: string
|
|
188
|
+
routes: RouteManifestEntry[]
|
|
189
|
+
conflicts: RouteManifestConflict[]
|
|
190
|
+
totals: Record<RenderMode, number>
|
|
191
|
+
} = { routes: entries, conflicts, totals }
|
|
192
|
+
if (options.target !== undefined) result.target = options.target
|
|
193
|
+
return result as RouteManifest
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Render the manifest as a readable report - the `nifra routes --modes` output. */
|
|
197
|
+
export function renderRouteManifest(manifest: RouteManifest): string {
|
|
198
|
+
const lines: string[] = []
|
|
199
|
+
lines.push(
|
|
200
|
+
manifest.target === undefined
|
|
201
|
+
? "nifra route manifest"
|
|
202
|
+
: `nifra route manifest - target: ${manifest.target}`,
|
|
203
|
+
)
|
|
204
|
+
lines.push("")
|
|
205
|
+
if (manifest.routes.length === 0) {
|
|
206
|
+
lines.push("no routes found.")
|
|
207
|
+
return lines.join("\n")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const width = Math.max(10, ...manifest.routes.map((r) => r.pattern.length)) + 2
|
|
211
|
+
lines.push(`${"route".padEnd(width)}${"mode".padEnd(9)}${"hydrate".padEnd(9)}cache`)
|
|
212
|
+
lines.push("-".repeat(width + 18 + 24))
|
|
213
|
+
for (const route of manifest.routes) {
|
|
214
|
+
const cache =
|
|
215
|
+
route.mode === "isr"
|
|
216
|
+
? `revalidate ${route.revalidate}s`
|
|
217
|
+
: route.mode === "static"
|
|
218
|
+
? `prerendered${route.prerenderedPaths ? ` (${route.prerenderedPaths.length})` : ""}`
|
|
219
|
+
: "per request"
|
|
220
|
+
lines.push(
|
|
221
|
+
route.pattern.padEnd(width) +
|
|
222
|
+
route.mode.padEnd(9) +
|
|
223
|
+
(route.hydrate ? "yes" : "no").padEnd(9) +
|
|
224
|
+
cache,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
lines.push("")
|
|
228
|
+
lines.push(
|
|
229
|
+
`${manifest.totals.static} static · ${manifest.totals.isr} isr · ${manifest.totals.ssr} ssr`,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (manifest.conflicts.length > 0) {
|
|
233
|
+
lines.push("")
|
|
234
|
+
lines.push(`✗ ${manifest.conflicts.length} route(s) the target cannot honour:`)
|
|
235
|
+
for (const conflict of manifest.conflicts) {
|
|
236
|
+
lines.push(` ✗ ${conflict.pattern} - ${conflict.consequence}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\n")
|
|
240
|
+
}
|