@barefootjs/vite 0.31.6 → 0.31.8
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/index.js +105 -11
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3916,6 +3916,11 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
3916
3916
|
case "expression": {
|
|
3917
3917
|
if (node.expr === "null" || node.expr === "undefined")
|
|
3918
3918
|
return "";
|
|
3919
|
+
if (node.clientOnly && node.slotId) {
|
|
3920
|
+
if (node.markerless)
|
|
3921
|
+
return "";
|
|
3922
|
+
return `<!--bf:${node.slotId}--><!--/-->`;
|
|
3923
|
+
}
|
|
3919
3924
|
const wrapped = transformExpr(node.expr, node.templateExpr);
|
|
3920
3925
|
const value = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
|
|
3921
3926
|
if (node.slotId) {
|
|
@@ -5035,6 +5040,7 @@ var ErrorCodes = {
|
|
|
5035
5040
|
COMPONENT_REQUIRED_PROP_MISSING: "BF046",
|
|
5036
5041
|
JSX_BRANCH_LOCAL_IN_CALLBACK: "BF047",
|
|
5037
5042
|
SIBLING_COMPONENT_NOT_COMPILED: "BF048",
|
|
5043
|
+
RICH_TYPE_PROP_NOT_HYDRATABLE: "BF049",
|
|
5038
5044
|
SHARED_PROGRAM_REQUIRED: "BF050",
|
|
5039
5045
|
WRONG_PACKAGE_IMPORT: "BF051",
|
|
5040
5046
|
BUILTIN_REQUIRES_IMPORT: "BF054",
|
|
@@ -5065,6 +5071,7 @@ var errorMessages = {
|
|
|
5065
5071
|
[ErrorCodes.COMPONENT_REQUIRED_PROP_MISSING]: "Built-in component is missing a required prop.",
|
|
5066
5072
|
[ErrorCodes.JSX_BRANCH_LOCAL_IN_CALLBACK]: "JSX-typed local declared inside an `if`-block cannot be referenced from a callback body (ref / event handler). " + "Render it as a child instead: `<div ref={...}>{local}</div>`.",
|
|
5067
5073
|
[ErrorCodes.SIBLING_COMPONENT_NOT_COMPILED]: "Referenced component did not compile to a template, so this reference would throw " + "`ReferenceError` at render time. Multi-return JSX dispatch (a `switch` or `if`/`else` " + "chain across multiple JSX-returning branches) cannot compile as a component in a " + `'use client' file. Extract it to a separate non-"use client" file (where it is preserved ` + "verbatim, see #932), or rewrite it as a single-return ternary/conditional chain so the " + "component pipeline can compile it.",
|
|
5074
|
+
[ErrorCodes.RICH_TYPE_PROP_NOT_HYDRATABLE]: "Rich-typed prop cannot cross the bf-p hydration boundary as JSON.",
|
|
5068
5075
|
[ErrorCodes.SHARED_PROGRAM_REQUIRED]: "Shared ts.Program required for type-based reactivity classification. This source imports a Reactive<T>-branded library (e.g. @barefootjs/form) whose getters cannot be classified by regex alone. Pass `options.program` (built via `createProgramForCorpus`) so the analyzer can resolve the brand through the TypeChecker.",
|
|
5069
5076
|
[ErrorCodes.WRONG_PACKAGE_IMPORT]: "Import from wrong package.",
|
|
5070
5077
|
[ErrorCodes.BUILTIN_REQUIRES_IMPORT]: "Built-in <Async> / <Region> must be imported from '@barefootjs/client'. " + "The compiler recognises these tags by their import (not by tag name), " + "so an unimported tag with this name is treated as an undeclared component.",
|
|
@@ -5182,6 +5189,8 @@ var HOST_RICH_TYPE_NAMES = new Set([
|
|
|
5182
5189
|
"BigInt",
|
|
5183
5190
|
"Function"
|
|
5184
5191
|
]);
|
|
5192
|
+
var JSON_REVIVABLE_RICH_TYPE_NAMES = new Set(["Date", "URL"]);
|
|
5193
|
+
var JSON_UNSAFE_RICH_TYPE_NAMES = new Set([...HOST_RICH_TYPE_NAMES].filter((n) => !JSON_REVIVABLE_RICH_TYPE_NAMES.has(n)));
|
|
5185
5194
|
function baseTypeName(raw) {
|
|
5186
5195
|
const idx = raw.indexOf("<");
|
|
5187
5196
|
return (idx === -1 ? raw : raw.slice(0, idx)).trim();
|
|
@@ -5214,6 +5223,21 @@ function lookupProperty(objType, propName, meta) {
|
|
|
5214
5223
|
const prop = deref.properties?.find((p) => p.name === propName);
|
|
5215
5224
|
return prop ? stripUnion(prop.type) : null;
|
|
5216
5225
|
}
|
|
5226
|
+
function resolvePropDeclaredType(propName, meta) {
|
|
5227
|
+
return lookupProperty(meta.propsType, propName, meta);
|
|
5228
|
+
}
|
|
5229
|
+
function jsonUnsafeTypeName(type) {
|
|
5230
|
+
if (!type)
|
|
5231
|
+
return null;
|
|
5232
|
+
if (type.kind === "interface") {
|
|
5233
|
+
const name = baseTypeName(type.raw);
|
|
5234
|
+
return JSON_UNSAFE_RICH_TYPE_NAMES.has(name) ? name : null;
|
|
5235
|
+
}
|
|
5236
|
+
if (type.kind === "unknown" && (type.raw === "bigint" || type.raw === "symbol")) {
|
|
5237
|
+
return type.raw;
|
|
5238
|
+
}
|
|
5239
|
+
return null;
|
|
5240
|
+
}
|
|
5217
5241
|
function resolveReceiverType(expr, meta, bindings) {
|
|
5218
5242
|
if (expr.kind === "identifier") {
|
|
5219
5243
|
if (bindings.has(expr.name))
|
|
@@ -14977,6 +15001,9 @@ function buildReferencesGraph(ctx, irRoot) {
|
|
|
14977
15001
|
addExprEdges(ROOT_SOURCE, event.handler, "init-body");
|
|
14978
15002
|
}
|
|
14979
15003
|
}
|
|
15004
|
+
for (const elem of ctx.clientOnlyElements) {
|
|
15005
|
+
addExprEdges(ROOT_SOURCE, elem.expression, "init-body");
|
|
15006
|
+
}
|
|
14980
15007
|
for (const elem of ctx.loopElements) {
|
|
14981
15008
|
addExprEdges(ROOT_SOURCE, elem.array, "template-closure");
|
|
14982
15009
|
addTemplateEdges(ROOT_SOURCE, elem.template, "template-closure");
|
|
@@ -16364,6 +16391,13 @@ function csrInlinableConstantsFromCtx(ctx) {
|
|
|
16364
16391
|
}
|
|
16365
16392
|
return out;
|
|
16366
16393
|
}
|
|
16394
|
+
function buildTemplateDefPart(ctx, templateHtml) {
|
|
16395
|
+
const envDecls = ctx.signals.filter((s) => Boolean(s.envReader) && Boolean(s.envFactory)).map((s) => `const [${s.getter}] = ${s.envFactory}()`);
|
|
16396
|
+
if (envDecls.length === 0) {
|
|
16397
|
+
return `template: (${PROPS_PARAM}) => \`${templateHtml}\``;
|
|
16398
|
+
}
|
|
16399
|
+
return `template: (${PROPS_PARAM}) => { ${envDecls.join("; ")}; return \`${templateHtml}\` }`;
|
|
16400
|
+
}
|
|
16367
16401
|
function emitRegistrationAndHydration(lines, ctx, _ir, graph, inlinability) {
|
|
16368
16402
|
const name = ctx.componentName;
|
|
16369
16403
|
lines.push(`}`);
|
|
@@ -16380,13 +16414,13 @@ function emitRegistrationAndHydration(lines, ctx, _ir, graph, inlinability) {
|
|
|
16380
16414
|
if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
16381
16415
|
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName);
|
|
16382
16416
|
if (templateHtml) {
|
|
16383
|
-
defParts.push(
|
|
16417
|
+
defParts.push(buildTemplateDefPart(ctx, templateHtml));
|
|
16384
16418
|
}
|
|
16385
16419
|
} else {
|
|
16386
16420
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx);
|
|
16387
16421
|
const templateHtml = generateCsrTemplate(_ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots);
|
|
16388
16422
|
if (templateHtml) {
|
|
16389
|
-
defParts.push(
|
|
16423
|
+
defParts.push(buildTemplateDefPart(ctx, templateHtml));
|
|
16390
16424
|
}
|
|
16391
16425
|
}
|
|
16392
16426
|
if (isCommentScope) {
|
|
@@ -19393,9 +19427,15 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
|
|
|
19393
19427
|
}
|
|
19394
19428
|
return restore(result);
|
|
19395
19429
|
}
|
|
19396
|
-
function
|
|
19397
|
-
const
|
|
19430
|
+
function makeCataloguedCallLowerer(ctx) {
|
|
19431
|
+
const dateMatcher = getReactiveDateLoweringMatcher(ctx);
|
|
19398
19432
|
const toLocaleMatcher = getReactiveToLocaleMatcher(ctx);
|
|
19433
|
+
if (!dateMatcher && !toLocaleMatcher)
|
|
19434
|
+
return (expr) => expr;
|
|
19435
|
+
return (expr) => lowerToLocaleCallsInReactiveExpr(lowerDateCallsInReactiveExpr(expr, dateMatcher), toLocaleMatcher);
|
|
19436
|
+
}
|
|
19437
|
+
function emitDynamicTextUpdates(lines, ctx) {
|
|
19438
|
+
const lower = makeCataloguedCallLowerer(ctx);
|
|
19399
19439
|
const byExpression = new Map;
|
|
19400
19440
|
for (const elem of ctx.dynamicElements) {
|
|
19401
19441
|
const key = elem.expression;
|
|
@@ -19405,7 +19445,7 @@ function emitDynamicTextUpdates(lines, ctx) {
|
|
|
19405
19445
|
byExpression.get(key).push(elem);
|
|
19406
19446
|
}
|
|
19407
19447
|
for (const [rawExpr, elems] of byExpression) {
|
|
19408
|
-
const expr =
|
|
19448
|
+
const expr = lower(rawExpr);
|
|
19409
19449
|
const conditionalElems = elems.filter((e) => e.insideConditional);
|
|
19410
19450
|
const normalElems = elems.filter((e) => !e.insideConditional);
|
|
19411
19451
|
if (normalElems.length > 0 || conditionalElems.length > 0) {
|
|
@@ -19442,19 +19482,21 @@ function emitDynamicTextUpdates(lines, ctx) {
|
|
|
19442
19482
|
}
|
|
19443
19483
|
}
|
|
19444
19484
|
function emitClientOnlyExpressions(lines, ctx) {
|
|
19485
|
+
const lower = makeCataloguedCallLowerer(ctx);
|
|
19445
19486
|
for (const elem of ctx.clientOnlyElements) {
|
|
19446
19487
|
const slots = elem.elidedPath ? [{ id: elem.slotId, kind: "text", path: elem.elidedPath, markerless: true }] : [{ id: elem.slotId, kind: "text", path: [] }];
|
|
19447
19488
|
const writer = claimWriterVarName(slots, varSlotId);
|
|
19448
19489
|
lines.push(` // @client: ${elem.slotId}`);
|
|
19449
19490
|
lines.push(` { const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
|
|
19450
19491
|
lines.push(` createEffect(() => {`);
|
|
19451
|
-
lines.push(` ${writer}('${elem.slotId}', ${elem.expression})`);
|
|
19492
|
+
lines.push(` ${writer}('${elem.slotId}', ${lower(elem.expression)})`);
|
|
19452
19493
|
lines.push(` }${bindingIdArg(ctx, elem.slotId)}) }`);
|
|
19453
19494
|
lines.push("");
|
|
19454
19495
|
}
|
|
19455
19496
|
}
|
|
19456
19497
|
function emitReactiveAttributeUpdates(lines, ctx) {
|
|
19457
19498
|
if (ctx.reactiveAttrs.length > 0) {
|
|
19499
|
+
const lower = makeCataloguedCallLowerer(ctx);
|
|
19458
19500
|
const attrsBySlot = new Map;
|
|
19459
19501
|
for (const attr of ctx.reactiveAttrs) {
|
|
19460
19502
|
if (!attrsBySlot.has(attr.slotId)) {
|
|
@@ -19467,7 +19509,7 @@ function emitReactiveAttributeUpdates(lines, ctx) {
|
|
|
19467
19509
|
lines.push(` createEffect(() => {`);
|
|
19468
19510
|
lines.push(` if (_${v}) {`);
|
|
19469
19511
|
for (const attr of attrs) {
|
|
19470
|
-
const expression = rewriteDestructuredPropsInExpr(attr.expression, ctx);
|
|
19512
|
+
const expression = rewriteDestructuredPropsInExpr(lower(attr.expression), ctx);
|
|
19471
19513
|
for (const stmt of emitAttrUpdate(`_${v}`, attr.attrName, expression, attr)) {
|
|
19472
19514
|
lines.push(` ${stmt}`);
|
|
19473
19515
|
}
|
|
@@ -22893,6 +22935,38 @@ function checkRichTypeMethodCalls(root, metadata, errors) {
|
|
|
22893
22935
|
const seen = new Set;
|
|
22894
22936
|
walkNode2(root, metadata, EMPTY_BINDINGS2, matchers, errors, seen);
|
|
22895
22937
|
}
|
|
22938
|
+
function checkRichTypePropSerialization(root, metadata, errors, declLoc) {
|
|
22939
|
+
if (!metadata.propsType || !metadata.clientAnalysis?.needsInit)
|
|
22940
|
+
return;
|
|
22941
|
+
const usedProps = new Set(metadata.clientAnalysis.usedProps);
|
|
22942
|
+
const loc = declLoc ?? root.loc;
|
|
22943
|
+
for (const param of metadata.propsParams) {
|
|
22944
|
+
if (param.isRest || param.name.startsWith("on") || param.name.startsWith("__"))
|
|
22945
|
+
continue;
|
|
22946
|
+
if (!usedProps.has(param.name))
|
|
22947
|
+
continue;
|
|
22948
|
+
const declared = resolvePropDeclaredType(param.sourceName ?? param.name, metadata);
|
|
22949
|
+
const typeName = jsonUnsafeTypeName(declared);
|
|
22950
|
+
if (!typeName)
|
|
22951
|
+
continue;
|
|
22952
|
+
if (metadata.typeDefinitions.some((d) => d.name === typeName))
|
|
22953
|
+
continue;
|
|
22954
|
+
pushPropSerializationDiagnostic(errors, loc, param.name, typeName, declared.raw);
|
|
22955
|
+
}
|
|
22956
|
+
}
|
|
22957
|
+
function pushPropSerializationDiagnostic(errors, loc, propName, typeName, declaredRaw) {
|
|
22958
|
+
const consequence = typeName === "bigint" || typeName === "BigInt" ? "JSON.stringify throws at SSR render ('Do not know how to serialize a BigInt'), failing the whole page" : typeName === "symbol" || typeName === "Symbol" || typeName === "Function" ? "JSON.stringify drops the value entirely, so the client reads undefined at hydrate" : "it serializes de-riched (e.g. a Map or Set becomes {} with every entry silently dropped), so the client hydrates against corrupt data";
|
|
22959
|
+
errors.push({
|
|
22960
|
+
code: ErrorCodes.RICH_TYPE_PROP_NOT_HYDRATABLE,
|
|
22961
|
+
severity: "error",
|
|
22962
|
+
message: `Prop '${propName}' is typed '${declaredRaw}' and is read by this component's own client code, so it must cross the bf-p hydration boundary as JSON — and ${typeName} cannot: ${consequence}.`,
|
|
22963
|
+
loc,
|
|
22964
|
+
suggestion: {
|
|
22965
|
+
message: "Pre-compute a JSON-serializable value server-side — a string, number, boolean, array, or plain object " + "(e.g. pass [...map.entries()] and rebuild the Map client-side where needed) — and pass that as the prop " + "instead. /* @client */ is NOT an escape here: the prop still crosses the bf-p boundary as JSON and arrives " + "de-riched (#2636).",
|
|
22966
|
+
escape: [{ kind: "prop-precompute" }]
|
|
22967
|
+
}
|
|
22968
|
+
});
|
|
22969
|
+
}
|
|
22896
22970
|
function isLoweringClaimed(matchers, callee, args) {
|
|
22897
22971
|
return matchers.some((m) => m(callee, args) !== null);
|
|
22898
22972
|
}
|
|
@@ -22909,21 +22983,39 @@ function receiverRootIsProp(expr, bindings) {
|
|
|
22909
22983
|
root = root.object;
|
|
22910
22984
|
return root.kind === "identifier" && !bindings.has(root.name);
|
|
22911
22985
|
}
|
|
22986
|
+
function buildSuggestion(method, receiverPath, receiver, typeName) {
|
|
22987
|
+
const revivalExpr = receiverPath === "<expression>" ? `wrapping the receiver in new ${typeName}(...) before calling .${method}()` : `{/* @client */ new ${typeName}(${receiverPath}).${method}(...)}`;
|
|
22988
|
+
const revivalReason = `a bare /* @client */ crashes at hydrate because ${receiver} crosses the bf-p boundary as JSON and arrives as a plain string, not a ${typeName} instance (#2636)`;
|
|
22989
|
+
if (method === "toLocaleDateString" && typeName === "Date") {
|
|
22990
|
+
return {
|
|
22991
|
+
message: "Pass a literal locale and an explicit literal timeZone — .toLocaleDateString('ja-JP', { timeZone: 'UTC' }), a fixed '±HH:MM' offset, or a canonical IANA zone ID like 'Asia/Tokyo' (exact case; #2344) — to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. " + `Alternatively pre-compute server-side, or evaluate client-only by ${revivalExpr} — ${revivalReason}.`,
|
|
22992
|
+
escape: [{ kind: "rewrite" }, { kind: "prop-precompute" }, { kind: "client-directive" }]
|
|
22993
|
+
};
|
|
22994
|
+
}
|
|
22995
|
+
if (JSON_REVIVABLE_RICH_TYPE_NAMES.has(typeName)) {
|
|
22996
|
+
return {
|
|
22997
|
+
message: `Pre-compute the value server-side and pass it as a prop. Alternatively, evaluate client-only by ${revivalExpr} — ${revivalReason}.`,
|
|
22998
|
+
escape: [{ kind: "prop-precompute" }, { kind: "client-directive" }]
|
|
22999
|
+
};
|
|
23000
|
+
}
|
|
23001
|
+
return {
|
|
23002
|
+
message: `Pre-compute the value server-side and pass the result — a string, number, array, or plain object — as a prop. ` + `/* @client */ is NOT a safe escape here: ${receiver} cannot cross the bf-p hydration boundary as JSON — it arrives de-riched ` + `(e.g. a Map or Set serializes to {}), so the call throws or silently returns the wrong result at hydrate (#2636).`,
|
|
23003
|
+
escape: [{ kind: "prop-precompute" }]
|
|
23004
|
+
};
|
|
23005
|
+
}
|
|
22912
23006
|
function pushDiagnostic(errors, seen, loc, method, receiverPath, isProp, typeName) {
|
|
22913
23007
|
const key = `${loc.start.line}:${loc.start.column}:${receiverPath}.${method}`;
|
|
22914
23008
|
if (seen.has(key))
|
|
22915
23009
|
return;
|
|
22916
23010
|
seen.add(key);
|
|
22917
23011
|
const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`;
|
|
22918
|
-
const suggestion = method
|
|
23012
|
+
const suggestion = buildSuggestion(method, receiverPath, receiver, typeName);
|
|
22919
23013
|
errors.push({
|
|
22920
23014
|
code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
22921
23015
|
severity: "error",
|
|
22922
23016
|
message: `Expression cannot be compiled to marked template: method '.${method}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
|
|
22923
23017
|
loc,
|
|
22924
|
-
suggestion
|
|
22925
|
-
message: suggestion
|
|
22926
|
-
}
|
|
23018
|
+
suggestion
|
|
22927
23019
|
});
|
|
22928
23020
|
}
|
|
22929
23021
|
function checkExpr(expr, loc, meta, bindings, matchers, errors, seen) {
|
|
@@ -23168,6 +23260,7 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
|
|
|
23168
23260
|
};
|
|
23169
23261
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
23170
23262
|
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
|
|
23263
|
+
checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx.propsDestructuring?.loc);
|
|
23171
23264
|
decideClientOnlyElision(componentIR.root);
|
|
23172
23265
|
entries.push({ componentIR, ctx });
|
|
23173
23266
|
}
|
|
@@ -23570,6 +23663,7 @@ function compileJSX(source, filePath, options) {
|
|
|
23570
23663
|
};
|
|
23571
23664
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
23572
23665
|
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
|
|
23666
|
+
checkRichTypePropSerialization(componentIR.root, componentIR.metadata, errors, ctx.propsDestructuring?.loc);
|
|
23573
23667
|
decideClientOnlyElision(componentIR.root);
|
|
23574
23668
|
if (ctx.importedClientSignalNames.size > 0) {
|
|
23575
23669
|
const sources = new Set;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/vite",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.8",
|
|
4
4
|
"description": "Vite plugin for BarefootJS: Vite/Rollup owns bundling, hashing, chunking, tree-shaking and minification of client assets, BarefootJS keeps only the JSX to (template, client JS) compile",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,17 +38,17 @@
|
|
|
38
38
|
"directory": "packages/vite"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@barefootjs/shared": "0.31.
|
|
41
|
+
"@barefootjs/shared": "0.31.8"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@barefootjs/jsx": ">=0.2.0",
|
|
45
45
|
"vite": "^6.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@barefootjs/client": "0.31.
|
|
49
|
-
"@barefootjs/go-template": "0.31.
|
|
50
|
-
"@barefootjs/hono": "0.31.
|
|
51
|
-
"@barefootjs/jsx": "0.31.
|
|
48
|
+
"@barefootjs/client": "0.31.8",
|
|
49
|
+
"@barefootjs/go-template": "0.31.8",
|
|
50
|
+
"@barefootjs/hono": "0.31.8",
|
|
51
|
+
"@barefootjs/jsx": "0.31.8",
|
|
52
52
|
"typescript": "^5.0.0",
|
|
53
53
|
"vite": "^6.0.0"
|
|
54
54
|
}
|