@broxium/compiler 1.3.3 → 1.5.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 +125 -6
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +62 -7
- package/dist/index.mjs +62 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,60 @@ Source files are written to a temporary directory under `os.tmpdir()` before com
|
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
|
+
## `config.json` format
|
|
127
|
+
|
|
128
|
+
Every component must include a `config.json` file alongside `App.tsx`. This file defines the props exposed in the website builder's property inspector panel.
|
|
129
|
+
|
|
130
|
+
**The format is a flat object** — prop name as key, field definition as value. Do not use a `name`/`slug`/`props` wrapper.
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"title": {
|
|
135
|
+
"label": "Title",
|
|
136
|
+
"type": "text",
|
|
137
|
+
"default": "Hello World"
|
|
138
|
+
},
|
|
139
|
+
"count": {
|
|
140
|
+
"label": "Item Count",
|
|
141
|
+
"type": "number",
|
|
142
|
+
"default": 3
|
|
143
|
+
},
|
|
144
|
+
"theme": {
|
|
145
|
+
"label": "Theme",
|
|
146
|
+
"type": "select",
|
|
147
|
+
"options": ["light", "dark"],
|
|
148
|
+
"default": "light"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Supported field types
|
|
154
|
+
|
|
155
|
+
| `type` | Builder input | Extra keys |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `text` | Text input | — |
|
|
158
|
+
| `textarea` | Multiline textarea | — |
|
|
159
|
+
| `url` | URL input (validated) | — |
|
|
160
|
+
| `number` | Number input | — |
|
|
161
|
+
| `select` | Dropdown | `options: string[]` |
|
|
162
|
+
| `color` | Color picker + hex | — |
|
|
163
|
+
| `boolean` | Checkbox | — |
|
|
164
|
+
| `range` | Slider | `min`, `max` |
|
|
165
|
+
| `brodox-*` | Custom Brodox field | varies |
|
|
166
|
+
|
|
167
|
+
### Optional field keys
|
|
168
|
+
|
|
169
|
+
| Key | Type | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `label` | `string` | Display name in the inspector panel |
|
|
172
|
+
| `type` | `string` | Input type (required) |
|
|
173
|
+
| `default` | `any` | Initial value when component is first dropped |
|
|
174
|
+
| `render` | `"client"` \| `"server"` | Badge shown in inspector (cosmetic only) |
|
|
175
|
+
|
|
176
|
+
> **Common crash:** If `config.json` is not a flat object (e.g. has a top-level `name` or `props` array key), the builder will crash with `Cannot read properties of undefined (reading 'startsWith')` when the component is dragged onto the canvas.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
126
180
|
## `'use client'` and `'use server'` handling
|
|
127
181
|
|
|
128
182
|
The two esbuild builds use custom plugins to handle React-style directives:
|
|
@@ -153,11 +207,44 @@ Server-only code never runs in the browser.
|
|
|
153
207
|
|
|
154
208
|
The directive on the **entry file** (`App.tsx`) determines the `renderMode` stored in the Page Manifest:
|
|
155
209
|
|
|
156
|
-
| Entry file starts with | `renderMode` | Server renders | Client hydrates |
|
|
157
|
-
|
|
158
|
-
| `'use client'` | `client` | No | Yes |
|
|
159
|
-
| `'use server'` | `server` | Yes | No |
|
|
160
|
-
| _(nothing)_ | `both` | Yes | Yes |
|
|
210
|
+
| Entry file starts with | `renderMode` | Server renders | Client hydrates | Use when |
|
|
211
|
+
|---|---|---|---|---|
|
|
212
|
+
| `'use client'` | `client` | No | Yes (full) | `useState`, `useEffect`, event handlers, browser APIs |
|
|
213
|
+
| `'use server'` | `server` | Yes (full) | No | Display-only, no interactivity needed |
|
|
214
|
+
| _(nothing)_ | `both` | Yes | Yes (islands only) | Static shell with `<Client>` islands for interactive parts |
|
|
215
|
+
|
|
216
|
+
> **Note:** These directives are **not** the same as Next.js. `'use server'` here means "exclude from client bundle" — it does not create a server action.
|
|
217
|
+
|
|
218
|
+
### Common mistake — missing `'use client'`
|
|
219
|
+
|
|
220
|
+
Using `useState`, `useEffect`, or any hook at the top level of the entry file **without** `'use client'` causes the web engine to crash during SSR:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Cannot read properties of null (reading 'useState')
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Fix: add `'use client'` as the very first line of `App.tsx`.
|
|
227
|
+
|
|
228
|
+
```jsx
|
|
229
|
+
'use client';
|
|
230
|
+
|
|
231
|
+
import { useState } from 'react';
|
|
232
|
+
|
|
233
|
+
export default function App() {
|
|
234
|
+
const [count, setCount] = useState(0);
|
|
235
|
+
// ...
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Sub-file directives
|
|
240
|
+
|
|
241
|
+
You can split a multi-file component by marking individual files:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
App.tsx ← no directive (both: SSR shell + client hydration)
|
|
245
|
+
├── Counter.tsx ← 'use client' (stubbed on server, runs in browser)
|
|
246
|
+
└── DataTable.tsx← 'use server' (stubbed in browser, runs on server)
|
|
247
|
+
```
|
|
161
248
|
|
|
162
249
|
---
|
|
163
250
|
|
|
@@ -238,14 +325,46 @@ When `BundleService` catches a compilation error, it blocks the component from b
|
|
|
238
325
|
|
|
239
326
|
---
|
|
240
327
|
|
|
328
|
+
## `runtimeServerStubPlugin` — @broxium/runtime server stubs
|
|
329
|
+
|
|
330
|
+
During server bundle compilation, all `@broxium/runtime` imports are replaced
|
|
331
|
+
by inline server-safe stubs so the server bundle has zero external dependencies.
|
|
332
|
+
|
|
333
|
+
Key stub behaviours:
|
|
334
|
+
|
|
335
|
+
| Export | Server stub renders |
|
|
336
|
+
|---|---|
|
|
337
|
+
| `BrodoxLink` | `<a data-brodox-link href={href}>` — the `data-brodox-link` attribute is required for the shell's click interceptor |
|
|
338
|
+
| `BrodoxImage` | `<img src="/api/image?...">` with srcset, or raw `src` if `direct={true}` |
|
|
339
|
+
| `Client` | Empty placeholder div + sibling `<script type="application/json">` with props |
|
|
340
|
+
| `Server` | Transparent passthrough (Fragment) |
|
|
341
|
+
| `useRouter`, `useParams` | Static no-ops (return empty objects) |
|
|
342
|
+
| `BrodoxHead`, `BrodoxFont` | null |
|
|
343
|
+
|
|
344
|
+
**Important:** If you compile a component and the rendered `<a>` tags do not
|
|
345
|
+
have `data-brodox-link`, the shell will not intercept clicks on those links and
|
|
346
|
+
the browser will do a full page reload. This was fixed in v1.3.2 — ensure all
|
|
347
|
+
projects use `@broxium/compiler@^1.3.2` or later. Existing pre-1.3.2 server
|
|
348
|
+
bundles must be patched manually or recompiled.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
241
352
|
## Package info
|
|
242
353
|
|
|
243
354
|
| | |
|
|
244
355
|
|---|---|
|
|
245
356
|
| Package | `@broxium/compiler` |
|
|
246
|
-
| Version | `1.
|
|
357
|
+
| Version | `1.3.3` |
|
|
247
358
|
| Formats | ESM (`dist/index.mjs`), CJS (`dist/index.js`) |
|
|
248
359
|
| Types | `dist/index.d.ts` |
|
|
249
360
|
| Runtime dependency | `esbuild ^0.25` |
|
|
250
361
|
| Node.js requirement | 20+ |
|
|
251
362
|
| Side effects | Writes files to `outputDir`, creates/removes temp directory |
|
|
363
|
+
|
|
364
|
+
## Changelog
|
|
365
|
+
|
|
366
|
+
| Version | Change |
|
|
367
|
+
|---|---|
|
|
368
|
+
| 1.3.3 | `BrodoxImage` stub: added `direct` prop support |
|
|
369
|
+
| 1.3.2 | `BrodoxLink` stub: added `data-brodox-link` attribute |
|
|
370
|
+
| 1.3.1 | Initial public release |
|
package/dist/index.d.mts
CHANGED
|
@@ -13,8 +13,12 @@ interface CompileInput {
|
|
|
13
13
|
interface CompileOutput {
|
|
14
14
|
serverJsPath: string;
|
|
15
15
|
clientJsPath: string;
|
|
16
|
+
/** Compiled CSS bundle path, or null if no CSS files were imported. */
|
|
17
|
+
cssPath: string | null;
|
|
16
18
|
serverJsName: string;
|
|
17
19
|
clientJsName: string;
|
|
20
|
+
/** CSS bundle filename, or null if no CSS files were imported. */
|
|
21
|
+
cssName: string | null;
|
|
18
22
|
compiledAt: Date;
|
|
19
23
|
}
|
|
20
24
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,12 @@ interface CompileInput {
|
|
|
13
13
|
interface CompileOutput {
|
|
14
14
|
serverJsPath: string;
|
|
15
15
|
clientJsPath: string;
|
|
16
|
+
/** Compiled CSS bundle path, or null if no CSS files were imported. */
|
|
17
|
+
cssPath: string | null;
|
|
16
18
|
serverJsName: string;
|
|
17
19
|
clientJsName: string;
|
|
20
|
+
/** CSS bundle filename, or null if no CSS files were imported. */
|
|
21
|
+
cssName: string | null;
|
|
18
22
|
compiledAt: Date;
|
|
19
23
|
}
|
|
20
24
|
|
package/dist/index.js
CHANGED
|
@@ -154,9 +154,29 @@ export function useRouter() {
|
|
|
154
154
|
|
|
155
155
|
export function useParams() { return {}; }
|
|
156
156
|
|
|
157
|
-
export function BrodoxHead(
|
|
157
|
+
export function BrodoxHead({ title, description }) {
|
|
158
|
+
if (title && typeof globalThis.__brodoxCollectHead === 'function')
|
|
159
|
+
globalThis.__brodoxCollectHead({ type: 'title', props: { content: title } });
|
|
160
|
+
if (description && typeof globalThis.__brodoxCollectHead === 'function')
|
|
161
|
+
globalThis.__brodoxCollectHead({ type: 'meta', props: { name: 'description', content: description } });
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
158
164
|
|
|
159
|
-
export function BrodoxFont(
|
|
165
|
+
export function BrodoxFont({ href, family, weights, display }) {
|
|
166
|
+
weights = weights || [400, 700];
|
|
167
|
+
display = display || 'swap';
|
|
168
|
+
var url = href;
|
|
169
|
+
if (!url && family) {
|
|
170
|
+
url = 'https://fonts.googleapis.com/css2?family='
|
|
171
|
+
+ encodeURIComponent(family) + ':wght@' + weights.join(';') + '&display=' + display;
|
|
172
|
+
}
|
|
173
|
+
if (url && typeof globalThis.__brodoxCollectHead === 'function') {
|
|
174
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } });
|
|
175
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' } });
|
|
176
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'stylesheet', href: url } });
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
160
180
|
|
|
161
181
|
export function BrodoxRouter({ children }) {
|
|
162
182
|
return createElement(Fragment, null, children);
|
|
@@ -170,7 +190,7 @@ export function BrodoxRouter({ children }) {
|
|
|
170
190
|
* The IslandHydrator walks the live DOM and mounts the component from the
|
|
171
191
|
* parent bundle's __registry__ after page load.
|
|
172
192
|
*/
|
|
173
|
-
export function Client({ children }) {
|
|
193
|
+
export function Client({ children, hydration = 'load' }) {
|
|
174
194
|
var id = __nextIslandId();
|
|
175
195
|
var child = Children.only(children);
|
|
176
196
|
var compType = child.type;
|
|
@@ -185,13 +205,13 @@ export function Client({ children }) {
|
|
|
185
205
|
return createElement(Fragment, null,
|
|
186
206
|
createElement('div', {
|
|
187
207
|
'data-brodox-island': id,
|
|
188
|
-
'data-hydration':
|
|
208
|
+
'data-hydration': hydration,
|
|
189
209
|
'data-client-js': '',
|
|
190
210
|
'data-component-slug': '',
|
|
191
211
|
'data-version': '',
|
|
192
212
|
'data-component': name,
|
|
193
213
|
}),
|
|
194
|
-
createElement('script', {
|
|
214
|
+
hydration === 'none' ? null : createElement('script', {
|
|
195
215
|
type: 'application/json',
|
|
196
216
|
'data-brodox-props': id,
|
|
197
217
|
dangerouslySetInnerHTML: { __html: safeProps },
|
|
@@ -303,7 +323,9 @@ var BrodoxCompiler = class {
|
|
|
303
323
|
outfile: serverJsPath,
|
|
304
324
|
minify: false,
|
|
305
325
|
sourcemap: false,
|
|
306
|
-
define: { "process.env.NODE_ENV": '"production"' }
|
|
326
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
327
|
+
loader: { ".css": "text" }
|
|
328
|
+
// CSS imports return empty string in server bundle
|
|
307
329
|
});
|
|
308
330
|
const clientComponents = [];
|
|
309
331
|
for (const file of input.files) {
|
|
@@ -341,14 +363,47 @@ ${registryEntries}
|
|
|
341
363
|
minify: true,
|
|
342
364
|
sourcemap: false,
|
|
343
365
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
344
|
-
banner: { js: 'import React from "react";' }
|
|
366
|
+
banner: { js: 'import React from "react";' },
|
|
367
|
+
loader: { ".css": "text" }
|
|
368
|
+
// CSS imports return the CSS string (injected via BrodoxHead or style tag)
|
|
345
369
|
});
|
|
370
|
+
const hasCss = input.files.some((f) => /\.css$/.test(f.path));
|
|
371
|
+
const cssName = hasCss ? `${safeName}.css` : null;
|
|
372
|
+
const cssPath = hasCss ? import_node_path3.default.join(input.outputDir, cssName) : null;
|
|
373
|
+
if (hasCss && cssPath) {
|
|
374
|
+
try {
|
|
375
|
+
await esbuild.build({
|
|
376
|
+
entryPoints: [entryPoint],
|
|
377
|
+
bundle: true,
|
|
378
|
+
format: "esm",
|
|
379
|
+
platform: "browser",
|
|
380
|
+
jsx: "automatic",
|
|
381
|
+
external: CLIENT_EXTERNALS,
|
|
382
|
+
plugins: [serverStubPlugin()],
|
|
383
|
+
outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
|
|
384
|
+
// esbuild needs a JS outfile
|
|
385
|
+
minify: true,
|
|
386
|
+
sourcemap: false,
|
|
387
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
388
|
+
loader: { ".css": "css" }
|
|
389
|
+
});
|
|
390
|
+
const defaultCssOut = cssPath.replace(/\.css$/, ".css.tmp.css");
|
|
391
|
+
try {
|
|
392
|
+
await import_promises3.default.rename(defaultCssOut, cssPath);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
await import_promises3.default.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
346
399
|
await import_promises3.default.rm(tmpDir, { recursive: true, force: true });
|
|
347
400
|
return {
|
|
348
401
|
serverJsPath,
|
|
349
402
|
clientJsPath,
|
|
403
|
+
cssPath,
|
|
350
404
|
serverJsName,
|
|
351
405
|
clientJsName,
|
|
406
|
+
cssName,
|
|
352
407
|
compiledAt: /* @__PURE__ */ new Date()
|
|
353
408
|
};
|
|
354
409
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -125,9 +125,29 @@ export function useRouter() {
|
|
|
125
125
|
|
|
126
126
|
export function useParams() { return {}; }
|
|
127
127
|
|
|
128
|
-
export function BrodoxHead(
|
|
128
|
+
export function BrodoxHead({ title, description }) {
|
|
129
|
+
if (title && typeof globalThis.__brodoxCollectHead === 'function')
|
|
130
|
+
globalThis.__brodoxCollectHead({ type: 'title', props: { content: title } });
|
|
131
|
+
if (description && typeof globalThis.__brodoxCollectHead === 'function')
|
|
132
|
+
globalThis.__brodoxCollectHead({ type: 'meta', props: { name: 'description', content: description } });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
129
135
|
|
|
130
|
-
export function BrodoxFont(
|
|
136
|
+
export function BrodoxFont({ href, family, weights, display }) {
|
|
137
|
+
weights = weights || [400, 700];
|
|
138
|
+
display = display || 'swap';
|
|
139
|
+
var url = href;
|
|
140
|
+
if (!url && family) {
|
|
141
|
+
url = 'https://fonts.googleapis.com/css2?family='
|
|
142
|
+
+ encodeURIComponent(family) + ':wght@' + weights.join(';') + '&display=' + display;
|
|
143
|
+
}
|
|
144
|
+
if (url && typeof globalThis.__brodoxCollectHead === 'function') {
|
|
145
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } });
|
|
146
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' } });
|
|
147
|
+
globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'stylesheet', href: url } });
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
131
151
|
|
|
132
152
|
export function BrodoxRouter({ children }) {
|
|
133
153
|
return createElement(Fragment, null, children);
|
|
@@ -141,7 +161,7 @@ export function BrodoxRouter({ children }) {
|
|
|
141
161
|
* The IslandHydrator walks the live DOM and mounts the component from the
|
|
142
162
|
* parent bundle's __registry__ after page load.
|
|
143
163
|
*/
|
|
144
|
-
export function Client({ children }) {
|
|
164
|
+
export function Client({ children, hydration = 'load' }) {
|
|
145
165
|
var id = __nextIslandId();
|
|
146
166
|
var child = Children.only(children);
|
|
147
167
|
var compType = child.type;
|
|
@@ -156,13 +176,13 @@ export function Client({ children }) {
|
|
|
156
176
|
return createElement(Fragment, null,
|
|
157
177
|
createElement('div', {
|
|
158
178
|
'data-brodox-island': id,
|
|
159
|
-
'data-hydration':
|
|
179
|
+
'data-hydration': hydration,
|
|
160
180
|
'data-client-js': '',
|
|
161
181
|
'data-component-slug': '',
|
|
162
182
|
'data-version': '',
|
|
163
183
|
'data-component': name,
|
|
164
184
|
}),
|
|
165
|
-
createElement('script', {
|
|
185
|
+
hydration === 'none' ? null : createElement('script', {
|
|
166
186
|
type: 'application/json',
|
|
167
187
|
'data-brodox-props': id,
|
|
168
188
|
dangerouslySetInnerHTML: { __html: safeProps },
|
|
@@ -274,7 +294,9 @@ var BrodoxCompiler = class {
|
|
|
274
294
|
outfile: serverJsPath,
|
|
275
295
|
minify: false,
|
|
276
296
|
sourcemap: false,
|
|
277
|
-
define: { "process.env.NODE_ENV": '"production"' }
|
|
297
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
298
|
+
loader: { ".css": "text" }
|
|
299
|
+
// CSS imports return empty string in server bundle
|
|
278
300
|
});
|
|
279
301
|
const clientComponents = [];
|
|
280
302
|
for (const file of input.files) {
|
|
@@ -312,14 +334,47 @@ ${registryEntries}
|
|
|
312
334
|
minify: true,
|
|
313
335
|
sourcemap: false,
|
|
314
336
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
315
|
-
banner: { js: 'import React from "react";' }
|
|
337
|
+
banner: { js: 'import React from "react";' },
|
|
338
|
+
loader: { ".css": "text" }
|
|
339
|
+
// CSS imports return the CSS string (injected via BrodoxHead or style tag)
|
|
316
340
|
});
|
|
341
|
+
const hasCss = input.files.some((f) => /\.css$/.test(f.path));
|
|
342
|
+
const cssName = hasCss ? `${safeName}.css` : null;
|
|
343
|
+
const cssPath = hasCss ? path3.join(input.outputDir, cssName) : null;
|
|
344
|
+
if (hasCss && cssPath) {
|
|
345
|
+
try {
|
|
346
|
+
await esbuild.build({
|
|
347
|
+
entryPoints: [entryPoint],
|
|
348
|
+
bundle: true,
|
|
349
|
+
format: "esm",
|
|
350
|
+
platform: "browser",
|
|
351
|
+
jsx: "automatic",
|
|
352
|
+
external: CLIENT_EXTERNALS,
|
|
353
|
+
plugins: [serverStubPlugin()],
|
|
354
|
+
outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
|
|
355
|
+
// esbuild needs a JS outfile
|
|
356
|
+
minify: true,
|
|
357
|
+
sourcemap: false,
|
|
358
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
359
|
+
loader: { ".css": "css" }
|
|
360
|
+
});
|
|
361
|
+
const defaultCssOut = cssPath.replace(/\.css$/, ".css.tmp.css");
|
|
362
|
+
try {
|
|
363
|
+
await fs3.rename(defaultCssOut, cssPath);
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
await fs3.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
}
|
|
317
370
|
await fs3.rm(tmpDir, { recursive: true, force: true });
|
|
318
371
|
return {
|
|
319
372
|
serverJsPath,
|
|
320
373
|
clientJsPath,
|
|
374
|
+
cssPath,
|
|
321
375
|
serverJsName,
|
|
322
376
|
clientJsName,
|
|
377
|
+
cssName,
|
|
323
378
|
compiledAt: /* @__PURE__ */ new Date()
|
|
324
379
|
};
|
|
325
380
|
}
|