rsbuild-plugin-react-router 0.6.6 → 0.7.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 +22 -0
- package/dist/511.js +10 -3
- package/dist/environment-output.d.ts +16 -2
- package/dist/index.cjs +100 -71
- package/dist/index.js +85 -64
- package/dist/manifest.d.ts +2 -0
- package/dist/mode-plan.d.ts +1 -1
- package/dist/plugin-utils.d.ts +19 -13
- package/dist/rsc-virtual-modules.d.ts +1 -3
- package/package.json +1 -1
- package/src/environment-output.ts +50 -1
- package/src/index.ts +43 -61
- package/src/manifest.ts +26 -20
- package/src/mode-plan.ts +6 -12
- package/src/modify-browser-manifest.ts +4 -4
- package/src/plugin-utils.ts +37 -23
- package/src/rsc-route-transforms.ts +8 -12
- package/src/rsc-virtual-modules.ts +51 -8
package/src/manifest.ts
CHANGED
|
@@ -167,19 +167,17 @@ type ReactRouterManifestStatsCompilation = {
|
|
|
167
167
|
entrypoints?: ReactRouterManifestStatsLookup<ReactRouterManifestStatsEntrypoint>;
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
];
|
|
182
|
-
};
|
|
170
|
+
// Emitted asset names may carry a query (`output.filename.js:
|
|
171
|
+
// '[name].js?v=[contenthash:8]'`); classify on the pathname but keep the full
|
|
172
|
+
// reference, since the query is part of the URL the browser must request.
|
|
173
|
+
// A chunk's own script is whatever JavaScript the compilation rendered for it,
|
|
174
|
+
// regardless of `output.filename` scheme. In development, HMR update files are
|
|
175
|
+
// also recorded on `chunk.files`; they are never the module to load.
|
|
176
|
+
export const isManifestJsAsset = (asset: string): boolean =>
|
|
177
|
+
/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset);
|
|
178
|
+
|
|
179
|
+
export const isManifestCssAsset = (asset: string): boolean =>
|
|
180
|
+
/\.css(?:\?.*)?$/.test(asset);
|
|
183
181
|
|
|
184
182
|
const collectManifestFilesByName = <T>(
|
|
185
183
|
items: ReactRouterManifestStatsLookup<T>,
|
|
@@ -241,8 +239,7 @@ export const createReactRouterManifestStats = (
|
|
|
241
239
|
const assetsByChunkName = collectManifestFilesByName(
|
|
242
240
|
compilation.namedChunks,
|
|
243
241
|
chunkNames,
|
|
244
|
-
(
|
|
245
|
-
orderChunkFiles(chunkName, Array.from(chunk.files ?? []))
|
|
242
|
+
(_chunkName, chunk) => Array.from(chunk.files ?? [])
|
|
246
243
|
);
|
|
247
244
|
const entrypointFilesByName = compilation.entrypoints
|
|
248
245
|
? collectManifestFilesByName(
|
|
@@ -303,22 +300,31 @@ const createChunkAssetResolver = (
|
|
|
303
300
|
|
|
304
301
|
const cssAssets = new Set<string>();
|
|
305
302
|
const jsAssets = new Set<string>();
|
|
303
|
+
// The chunk's own files come first so `js[0]` is the route module itself;
|
|
304
|
+
// entrypoint files (runtime, shared vendor chunks) follow as `imports`.
|
|
306
305
|
for (const asset of assets) {
|
|
307
|
-
if (asset
|
|
306
|
+
if (isManifestCssAsset(asset)) {
|
|
308
307
|
cssAssets.add(asset);
|
|
309
|
-
} else if (asset
|
|
308
|
+
} else if (isManifestJsAsset(asset)) {
|
|
310
309
|
jsAssets.add(asset);
|
|
311
310
|
}
|
|
312
311
|
}
|
|
313
312
|
for (const asset of clientStats?.entrypointFilesByName?.[chunkName] ?? []) {
|
|
314
|
-
if (asset
|
|
313
|
+
if (isManifestCssAsset(asset)) {
|
|
315
314
|
cssAssets.add(asset);
|
|
316
|
-
} else if (includeEntrypointJs && asset
|
|
315
|
+
} else if (includeEntrypointJs && isManifestJsAsset(asset)) {
|
|
317
316
|
jsAssets.add(asset);
|
|
318
317
|
}
|
|
319
318
|
}
|
|
320
319
|
if (jsAssets.size === 0) {
|
|
321
|
-
|
|
320
|
+
// Compilation metadata exists for this chunk but names no module script.
|
|
321
|
+
// Guessing `<dir>/<chunk>.js` here would turn an identifiable build
|
|
322
|
+
// problem into a browser 404, so surface it at build time instead.
|
|
323
|
+
throw new Error(
|
|
324
|
+
`[react-router] Chunk "${chunkName}" emitted no JavaScript asset the browser manifest can reference (files: ${
|
|
325
|
+
assets.join(', ') || 'none'
|
|
326
|
+
}). Check the web \`output.filename.js\` scheme.`
|
|
327
|
+
);
|
|
322
328
|
}
|
|
323
329
|
|
|
324
330
|
const result = { js: [...jsAssets], css: [...cssAssets] };
|
package/src/mode-plan.ts
CHANGED
|
@@ -36,10 +36,7 @@ type CommonModePlan = {
|
|
|
36
36
|
manifestChunkNames: Set<string>;
|
|
37
37
|
webEntries: Record<string, string | RsbuildEntryDescription>;
|
|
38
38
|
nodeEntries: Record<string, string | RsbuildEntryDescription>;
|
|
39
|
-
createVirtualModules(
|
|
40
|
-
publicPath: string,
|
|
41
|
-
jsDistPath: string
|
|
42
|
-
): Record<string, string>;
|
|
39
|
+
createVirtualModules(publicPath: string): Record<string, string>;
|
|
43
40
|
createResolveConfig(rootPath: string): Rspack.Configuration['resolve'];
|
|
44
41
|
server: RsbuildConfig['server'] | undefined;
|
|
45
42
|
webExternalsType: 'module' | undefined;
|
|
@@ -183,14 +180,13 @@ const createRscModePlan = async ({
|
|
|
183
180
|
layer: RSC_LAYERS.rsc,
|
|
184
181
|
},
|
|
185
182
|
},
|
|
186
|
-
createVirtualModules: (publicPath: string
|
|
183
|
+
createVirtualModules: (publicPath: string) =>
|
|
187
184
|
createReactRouterRscVirtualModules({
|
|
188
185
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
189
186
|
appDirectory,
|
|
190
187
|
basename,
|
|
191
188
|
buildDirectory,
|
|
192
189
|
isBuild,
|
|
193
|
-
jsDistPath,
|
|
194
190
|
outputClientPath,
|
|
195
191
|
publicPath,
|
|
196
192
|
routeDiscovery,
|
|
@@ -314,7 +310,7 @@ const createClassicModePlan = async ({
|
|
|
314
310
|
defaultEntryName,
|
|
315
311
|
serverBundleEntries: artifacts.serverBundleEntries,
|
|
316
312
|
}),
|
|
317
|
-
createVirtualModules: (publicPath: string
|
|
313
|
+
createVirtualModules: (publicPath: string) =>
|
|
318
314
|
createClassicVirtualModules({
|
|
319
315
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
320
316
|
appDirectory,
|
|
@@ -349,11 +345,9 @@ const createClassicModePlan = async ({
|
|
|
349
345
|
wasmLoading: 'fetch',
|
|
350
346
|
library: { type: 'module' },
|
|
351
347
|
module: true,
|
|
352
|
-
// Async
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
? { chunkFilename: 'static/js/async/[id]-[contenthash:16].js' }
|
|
356
|
-
: {}),
|
|
348
|
+
// Async chunk filenames are left to Rsbuild, which derives them from
|
|
349
|
+
// `output.filename.js`, `output.filenameHash`, and `distPath.jsAsync`,
|
|
350
|
+
// so user configuration governs every JavaScript filename (#129).
|
|
357
351
|
},
|
|
358
352
|
webOptimization: {
|
|
359
353
|
avoidEntryIife: true,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
generateReactRouterManifestForDev,
|
|
8
8
|
getReactRouterManifestChunkNames,
|
|
9
9
|
getReactRouterManifestPath,
|
|
10
|
+
isManifestJsAsset,
|
|
10
11
|
type ReactRouterManifestForDev as ReactRouterManifest,
|
|
11
12
|
type RouteChunkManifestOptions,
|
|
12
13
|
type RouteManifestModuleExports,
|
|
@@ -83,7 +84,7 @@ const addIntegrity = (
|
|
|
83
84
|
) => {
|
|
84
85
|
if (
|
|
85
86
|
typeof assetName !== 'string' ||
|
|
86
|
-
!assetName
|
|
87
|
+
!isManifestJsAsset(assetName) ||
|
|
87
88
|
typeof integrity !== 'string'
|
|
88
89
|
) {
|
|
89
90
|
return;
|
|
@@ -117,7 +118,7 @@ export const collectSubresourceIntegrity = (
|
|
|
117
118
|
const assets =
|
|
118
119
|
compilation.getAssets() as readonly CompilationAssetWithIntegrity[];
|
|
119
120
|
for (const asset of assets) {
|
|
120
|
-
if (!asset.name
|
|
121
|
+
if (!isManifestJsAsset(asset.name)) {
|
|
121
122
|
continue;
|
|
122
123
|
}
|
|
123
124
|
addIntegrity(
|
|
@@ -200,8 +201,7 @@ export function registerModifyBrowserManifestAssets(
|
|
|
200
201
|
|
|
201
202
|
if (isBuild) {
|
|
202
203
|
const entryAssets = stats?.assetsByChunkName?.['entry.client'];
|
|
203
|
-
const entryJsAssets =
|
|
204
|
-
entryAssets?.filter(asset => asset.endsWith('.js')) || [];
|
|
204
|
+
const entryJsAssets = entryAssets?.filter(isManifestJsAsset) || [];
|
|
205
205
|
const manifestPath = getReactRouterManifestPath({
|
|
206
206
|
version: manifest.version,
|
|
207
207
|
isBuild: true,
|
package/src/plugin-utils.ts
CHANGED
|
@@ -79,34 +79,48 @@ export function normalizeAssetPrefix(assetPrefix?: string): string {
|
|
|
79
79
|
return assetPrefix.endsWith('/') ? assetPrefix : `${assetPrefix}/`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
type AssetPrefixConfig = {
|
|
83
|
+
dev?: { assetPrefix?: unknown };
|
|
84
|
+
output?: { assetPrefix?: unknown };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const asString = (value: unknown): string | undefined =>
|
|
88
|
+
typeof value === 'string' ? value : undefined;
|
|
89
|
+
|
|
90
|
+
const pickConfiguredAssetPrefix = (
|
|
91
|
+
{ dev, output }: AssetPrefixConfig,
|
|
92
|
+
isBuild: boolean
|
|
93
|
+
): string | undefined =>
|
|
94
|
+
isBuild
|
|
95
|
+
? asString(output?.assetPrefix)
|
|
96
|
+
: (asString(dev?.assetPrefix) ?? asString(output?.assetPrefix));
|
|
97
|
+
|
|
82
98
|
/**
|
|
83
|
-
* Resolve the asset prefix
|
|
84
|
-
*
|
|
85
|
-
* defaults from `server.base`, falling back to
|
|
86
|
-
* production build it is `output.assetPrefix`.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
99
|
+
* Resolve the absolute asset prefix the server build and browser manifest use
|
|
100
|
+
* for asset URLs. In development the effective prefix is `dev.assetPrefix`
|
|
101
|
+
* (which Rsbuild defaults from `server.base`, falling back to
|
|
102
|
+
* `output.assetPrefix`); in a production build it is `output.assetPrefix`.
|
|
103
|
+
*
|
|
104
|
+
* `fallbacks` are consulted in order (e.g. the root config after the web
|
|
105
|
+
* environment) when the preceding config only offers a prefix the server
|
|
106
|
+
* cannot use: `'auto'` and empty values are browser-runtime-only, so a root
|
|
107
|
+
* CDN prefix must survive a web `'auto'`. The choice happens before
|
|
108
|
+
* normalization so that `'auto'` is not first folded into `'/'`.
|
|
89
109
|
*
|
|
90
110
|
* `dev.assetPrefix` may be a boolean on the raw config (`false` disables it);
|
|
91
|
-
* boolean/`'auto'`/empty values normalize to the root prefix `'/'`.
|
|
111
|
+
* boolean/`'auto'`/empty values ultimately normalize to the root prefix `'/'`.
|
|
92
112
|
*/
|
|
93
|
-
export function resolveEffectiveAssetPrefix(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (config.isBuild) {
|
|
103
|
-
return normalizeAssetPrefix(outputPrefix);
|
|
113
|
+
export function resolveEffectiveAssetPrefix(
|
|
114
|
+
config: AssetPrefixConfig & { isBuild: boolean },
|
|
115
|
+
...fallbacks: AssetPrefixConfig[]
|
|
116
|
+
): string {
|
|
117
|
+
for (const candidate of [config, ...fallbacks]) {
|
|
118
|
+
const prefix = pickConfiguredAssetPrefix(candidate, config.isBuild);
|
|
119
|
+
if (prefix && prefix !== 'auto') {
|
|
120
|
+
return normalizeAssetPrefix(prefix);
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
typeof config.dev?.assetPrefix === 'string'
|
|
107
|
-
? config.dev.assetPrefix
|
|
108
|
-
: undefined;
|
|
109
|
-
return normalizeAssetPrefix(devPrefix ?? outputPrefix);
|
|
123
|
+
return '/';
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
export function createRouteId(file: string): string {
|
|
@@ -623,6 +623,12 @@ const createServerRouteEntry = async (
|
|
|
623
623
|
let needsReactImport = false;
|
|
624
624
|
let needsEnsureHmrImport = false;
|
|
625
625
|
let needsStyleEntryImport = false;
|
|
626
|
+
const pushStylesheetLinks = (entryCssFilesExpression: string): void => {
|
|
627
|
+
lines.push(` ...(${entryCssFilesExpression} ?? []).map(href =>`);
|
|
628
|
+
lines.push(
|
|
629
|
+
' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
|
|
630
|
+
);
|
|
631
|
+
};
|
|
626
632
|
|
|
627
633
|
// A client route whose default component is a client reference: its bundled
|
|
628
634
|
// (non-vanilla) side-effect CSS is orphaned in the initial browser entry
|
|
@@ -651,12 +657,7 @@ const createServerRouteEntry = async (
|
|
|
651
657
|
'export default function RscClientRouteWithStyles___(props) {'
|
|
652
658
|
);
|
|
653
659
|
lines.push(' return React.createElement(React.Fragment, null,');
|
|
654
|
-
|
|
655
|
-
` ...(${RSC_ROUTE_STYLE_ENTRY_EXPORT}.entryCssFiles ?? []).map(href =>`
|
|
656
|
-
);
|
|
657
|
-
lines.push(
|
|
658
|
-
' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
|
|
659
|
-
);
|
|
660
|
+
pushStylesheetLinks(`${RSC_ROUTE_STYLE_ENTRY_EXPORT}.entryCssFiles`);
|
|
660
661
|
lines.push(' React.createElement(RscClientRouteDefault___, props),');
|
|
661
662
|
lines.push(' );');
|
|
662
663
|
lines.push('}');
|
|
@@ -682,12 +683,7 @@ const createServerRouteEntry = async (
|
|
|
682
683
|
// graph contributes. Render those as stylesheet links at the top of the
|
|
683
684
|
// stream (mirrors upstream's `import.meta.viteRsc.loadCss()`); React's
|
|
684
685
|
// float support hoists them into `<head>`.
|
|
685
|
-
|
|
686
|
-
` ...(${exportName}WithoutClientChunk.entryCssFiles ?? []).map(href =>`
|
|
687
|
-
);
|
|
688
|
-
lines.push(
|
|
689
|
-
' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
|
|
690
|
-
);
|
|
686
|
+
pushStylesheetLinks(`${exportName}WithoutClientChunk.entryCssFiles`);
|
|
691
687
|
lines.push(
|
|
692
688
|
' React.createElement(EnsureClientRouteModuleForHMR___, null),'
|
|
693
689
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, resolve } from 'pathe';
|
|
2
2
|
import type { Config } from './react-router-config.js';
|
|
3
3
|
import type { Route } from './types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeAssetPrefix } from './plugin-utils.js';
|
|
5
5
|
import { getVirtualModuleFilePath } from './virtual-modules.js';
|
|
6
6
|
import {
|
|
7
7
|
createRscInternalClientModule,
|
|
@@ -19,6 +19,7 @@ const RSC_VIRTUAL_ALIAS_IDS = [
|
|
|
19
19
|
'allowed-action-origins',
|
|
20
20
|
'client-version',
|
|
21
21
|
'react-router-serve-config',
|
|
22
|
+
'manifest-prefix',
|
|
22
23
|
'bootstrap-scripts',
|
|
23
24
|
'server-manifest',
|
|
24
25
|
] as const;
|
|
@@ -29,8 +30,6 @@ type RscVirtualModulesOptions = {
|
|
|
29
30
|
basename: string;
|
|
30
31
|
buildDirectory: string;
|
|
31
32
|
isBuild: boolean;
|
|
32
|
-
/** Resolved web `output.distPath.js` segment, e.g. `static/js`. */
|
|
33
|
-
jsDistPath: string;
|
|
34
33
|
outputClientPath: string;
|
|
35
34
|
publicPath: string;
|
|
36
35
|
routeDiscovery: Config['routeDiscovery'];
|
|
@@ -73,7 +72,6 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
73
72
|
basename,
|
|
74
73
|
buildDirectory,
|
|
75
74
|
isBuild,
|
|
76
|
-
jsDistPath,
|
|
77
75
|
outputClientPath,
|
|
78
76
|
publicPath,
|
|
79
77
|
routeDiscovery,
|
|
@@ -84,7 +82,10 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
84
82
|
resolve(buildDirectory, 'server'),
|
|
85
83
|
outputClientPath
|
|
86
84
|
);
|
|
87
|
-
|
|
85
|
+
// Absolute prefix the server must use for initial asset URLs (see
|
|
86
|
+
// `resolveEffectiveAssetPrefix`); the browser compiler may itself be on
|
|
87
|
+
// `'auto'`, which no server-rendered URL can express.
|
|
88
|
+
const serverPublicPath = normalizeAssetPrefix(publicPath);
|
|
88
89
|
|
|
89
90
|
return {
|
|
90
91
|
'virtual/react-router/unstable_rsc/routes': createRscRouteConfig({
|
|
@@ -111,9 +112,51 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
111
112
|
assetsBuildDirectory: rscAssetsBuildDirectory,
|
|
112
113
|
publicPath,
|
|
113
114
|
}),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// Every server-facing asset URL in the rspack RSC manifest -- bootstrap
|
|
116
|
+
// `entryJsFiles`, route `entryCssFiles`, client references' `cssFiles`
|
|
117
|
+
// (react-server-dom-rspack renders those as <link>s), and Flight's
|
|
118
|
+
// `moduleLoading.prefix` for client-chunk preloads -- carries the browser
|
|
119
|
+
// compiler's public path, recorded in `moduleLoading.prefix`. With the
|
|
120
|
+
// browser compiler on `'auto'` rspack records `/`, which the server cannot
|
|
121
|
+
// serve from. `__rspack_rsc_manifest__` is this same object, so aligning
|
|
122
|
+
// it once, in place (arrays are mutated, not replaced, because
|
|
123
|
+
// `createServerEntry` has already captured references), makes every
|
|
124
|
+
// consumer agree on the server prefix without stacking a second one. The
|
|
125
|
+
// aligned manifest is exported (not imported for side effects only) so a
|
|
126
|
+
// `sideEffects: false` package cannot tree-shake the alignment away.
|
|
127
|
+
'virtual/react-router/unstable_rsc/manifest-prefix': `const manifest = __webpack_require__.rscM;
|
|
128
|
+
const serverPrefix = ${JSON.stringify(serverPublicPath)};
|
|
129
|
+
const appliedPrefix = manifest?.moduleLoading?.prefix;
|
|
130
|
+
if (appliedPrefix && appliedPrefix !== serverPrefix) {
|
|
131
|
+
const rewrite = url =>
|
|
132
|
+
typeof url === "string" && url.startsWith(appliedPrefix)
|
|
133
|
+
? serverPrefix + url.slice(appliedPrefix.length)
|
|
134
|
+
: url;
|
|
135
|
+
const rewriteAll = list => {
|
|
136
|
+
if (Array.isArray(list)) for (let i = 0; i < list.length; i++) list[i] = rewrite(list[i]);
|
|
137
|
+
};
|
|
138
|
+
manifest.moduleLoading.prefix = serverPrefix;
|
|
139
|
+
rewriteAll(manifest.entryJsFiles);
|
|
140
|
+
for (const files of Object.values(manifest.entryCssFiles ?? {})) rewriteAll(files);
|
|
141
|
+
for (const reference of Object.values(manifest.clientManifest ?? {})) rewriteAll(reference.cssFiles);
|
|
142
|
+
}
|
|
143
|
+
export const rscManifest = manifest;
|
|
144
|
+
`,
|
|
145
|
+
// The compiled browser entry scripts come from the manifest (like Next's
|
|
146
|
+
// `buildManifest` or the Vite plugin's `loadBootstrapScriptContent`), so
|
|
147
|
+
// hashed entry filenames resolve without any naming contract. The list and
|
|
148
|
+
// its order are preserved. An empty list is a build problem (rspack only
|
|
149
|
+
// records entry files named `*.js`); fail loudly instead of guessing a
|
|
150
|
+
// filename that would render a document which cannot hydrate.
|
|
151
|
+
'virtual/react-router/unstable_rsc/bootstrap-scripts': `import { rscManifest } from "virtual/react-router/unstable_rsc/manifest-prefix";
|
|
152
|
+
const entryJsFiles = rscManifest?.entryJsFiles;
|
|
153
|
+
if (!entryJsFiles?.length) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"[rsbuild-plugin-react-router] The rspack RSC manifest lists no browser entry script (entryJsFiles is empty), so the server cannot render bootstrap scripts. Rspack only records entry files whose name ends in \\".js\\"; web output.filename.js values with a query (for example \\"[name].js?v=[contenthash:8]\\") or another extension are not supported in RSC mode."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
export default entryJsFiles;
|
|
159
|
+
`,
|
|
117
160
|
'virtual/react-router/unstable_rsc/server-manifest': `export default function getServerManifest() {
|
|
118
161
|
return __webpack_require__.rscM?.serverManifest;
|
|
119
162
|
}
|