@ezetgalaxy/titan 26.15.0 → 26.15.1
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/package.json +3 -2
- package/templates/common/Dockerfile +1 -1
- package/templates/common/app/t.native.d.ts +60 -0
- package/templates/js/package.json +1 -0
- package/templates/js/titan/bundle.js +194 -199
- package/templates/ts/package.json +1 -0
- package/templates/ts/titan/bundle.js +69 -19
- package/titanpl-sdk/package.json +2 -1
- package/titanpl-sdk/templates/app/app.js +1 -4
- package/titanpl-sdk/templates/titan/bundle.js +194 -199
- package/titanpl-sdk/templates/titan/dev.js +44 -4
- package/titanpl-sdk/templates/titan/error-box.js +10 -1
- package/titanpl-sdk/templates/app/t.native.d.ts +0 -1983
- package/titanpl-sdk/templates/app/t.native.js +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ezetgalaxy/titan",
|
|
3
|
-
"version": "26.15.
|
|
3
|
+
"version": "26.15.1",
|
|
4
4
|
"description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "ezetgalaxy",
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"test:all": "vitest run && vitest run --config vitest.config.e2e.ts"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@titanpl/core": "
|
|
68
|
+
"@titanpl/core": "latest",
|
|
69
|
+
"@titanpl/node": "latest",
|
|
69
70
|
"chokidar": "^5.0.0",
|
|
70
71
|
"esbuild": "^0.27.2",
|
|
71
72
|
"prompts": "^2.4.2"
|
|
@@ -1058,6 +1058,8 @@ declare global {
|
|
|
1058
1058
|
* @see https://titan-docs-ez.vercel.app/docs/10-extensions — Extensions documentation
|
|
1059
1059
|
*/
|
|
1060
1060
|
[key: string]: any;
|
|
1061
|
+
|
|
1062
|
+
|
|
1061
1063
|
}
|
|
1062
1064
|
|
|
1063
1065
|
/**
|
|
@@ -1978,6 +1980,64 @@ declare global {
|
|
|
1978
1980
|
}
|
|
1979
1981
|
|
|
1980
1982
|
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Node-compatible `process` global (Titan Shim).
|
|
1986
|
+
*
|
|
1987
|
+
* This is a lightweight compatibility layer intended
|
|
1988
|
+
* for supporting common Node libraries.
|
|
1989
|
+
*
|
|
1990
|
+
* Internally backed by:
|
|
1991
|
+
* - t.proc
|
|
1992
|
+
* - t.os
|
|
1993
|
+
* - t.time
|
|
1994
|
+
*/
|
|
1995
|
+
const process: {
|
|
1996
|
+
/** Process ID */
|
|
1997
|
+
pid: number;
|
|
1998
|
+
|
|
1999
|
+
/** Platform (linux, win32, darwin) */
|
|
2000
|
+
platform: string;
|
|
2001
|
+
|
|
2002
|
+
/** CPU architecture */
|
|
2003
|
+
arch: string;
|
|
2004
|
+
|
|
2005
|
+
/** Node version string (shimmed) */
|
|
2006
|
+
version: string;
|
|
2007
|
+
|
|
2008
|
+
/** Version object */
|
|
2009
|
+
versions: {
|
|
2010
|
+
node: string;
|
|
2011
|
+
titan: string;
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
/** Environment variables */
|
|
2015
|
+
env: Record<string, string | undefined>;
|
|
2016
|
+
|
|
2017
|
+
/** CLI arguments */
|
|
2018
|
+
argv: string[];
|
|
2019
|
+
|
|
2020
|
+
/** Current working directory */
|
|
2021
|
+
cwd(): string;
|
|
2022
|
+
|
|
2023
|
+
/** Uptime in seconds */
|
|
2024
|
+
uptime(): number;
|
|
2025
|
+
|
|
2026
|
+
/** High resolution time */
|
|
2027
|
+
hrtime: {
|
|
2028
|
+
(time?: [number, number]): [number, number];
|
|
2029
|
+
bigint(): bigint;
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
/** Memory usage info */
|
|
2033
|
+
memoryUsage(): Record<string, any>;
|
|
2034
|
+
|
|
2035
|
+
/** No-op event listener (compat only) */
|
|
2036
|
+
on(event: string, listener: (...args: any[]) => void): void;
|
|
2037
|
+
|
|
2038
|
+
/** Exit stub (throws in Titan runtime) */
|
|
2039
|
+
exit(code?: number): never;
|
|
2040
|
+
};
|
|
1981
2041
|
}
|
|
1982
2042
|
|
|
1983
2043
|
export { };
|
|
@@ -14,193 +14,204 @@ import { renderErrorBox, parseEsbuildError } from './error-box.js';
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
|
|
17
|
+
// Required for resolving node_modules inside ESM
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Titan Node Builtin Rewrite Map
|
|
22
|
+
* Rewrites Node builtins to @titanpl/node shims
|
|
23
|
+
*/
|
|
24
|
+
const NODE_BUILTIN_MAP = {
|
|
25
|
+
"fs": "@titanpl/node/fs",
|
|
26
|
+
"node:fs": "@titanpl/node/fs",
|
|
27
|
+
|
|
28
|
+
"path": "@titanpl/node/path",
|
|
29
|
+
"node:path": "@titanpl/node/path",
|
|
30
|
+
|
|
31
|
+
"os": "@titanpl/node/os",
|
|
32
|
+
"node:os": "@titanpl/node/os",
|
|
33
|
+
|
|
34
|
+
"crypto": "@titanpl/node/crypto",
|
|
35
|
+
"node:crypto": "@titanpl/node/crypto",
|
|
36
|
+
|
|
37
|
+
"process": "@titanpl/node/process",
|
|
38
|
+
|
|
39
|
+
"util": "@titanpl/node/util",
|
|
40
|
+
"node:util": "@titanpl/node/util",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Titan Node Compatibility Plugin
|
|
45
|
+
* Rewrites require/import of Node builtins
|
|
46
|
+
* Returns absolute paths (required by esbuild)
|
|
47
|
+
*/
|
|
48
|
+
const titanNodeCompatPlugin = {
|
|
49
|
+
name: "titan-node-compat",
|
|
50
|
+
setup(build) {
|
|
51
|
+
build.onResolve({ filter: /.*/ }, args => {
|
|
52
|
+
if (NODE_BUILTIN_MAP[args.path]) {
|
|
53
|
+
try {
|
|
54
|
+
const resolved = require.resolve(NODE_BUILTIN_MAP[args.path]);
|
|
55
|
+
return { path: resolved };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[Titan] Failed to resolve Node shim: ${NODE_BUILTIN_MAP[args.path]}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
17
66
|
/**
|
|
18
67
|
* Get Titan version for error branding
|
|
19
68
|
*/
|
|
20
69
|
function getTitanVersion() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
70
|
+
try {
|
|
71
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
72
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return "0.1.0";
|
|
75
|
+
}
|
|
28
76
|
}
|
|
29
77
|
|
|
30
78
|
/**
|
|
31
79
|
* Custom error class for bundle errors
|
|
32
80
|
*/
|
|
33
81
|
export class BundleError extends Error {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
82
|
+
constructor(message, errors = [], warnings = []) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = 'BundleError';
|
|
85
|
+
this.errors = errors;
|
|
86
|
+
this.warnings = warnings;
|
|
87
|
+
this.isBundleError = true;
|
|
88
|
+
}
|
|
41
89
|
}
|
|
42
90
|
|
|
43
91
|
/**
|
|
44
|
-
*
|
|
45
|
-
* @param {string} entryPoint - Entry file path
|
|
46
|
-
* @throws {BundleError} If file doesn't exist or isn't readable
|
|
92
|
+
* Validate entry file exists
|
|
47
93
|
*/
|
|
48
94
|
async function validateEntryPoint(entryPoint) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!fs.existsSync(absPath)) {
|
|
52
|
-
throw new BundleError(
|
|
53
|
-
`Entry point does not exist: ${entryPoint}`,
|
|
54
|
-
[{
|
|
55
|
-
text: `Cannot find file: ${absPath}`,
|
|
56
|
-
location: { file: entryPoint }
|
|
57
|
-
}]
|
|
58
|
-
);
|
|
59
|
-
}
|
|
95
|
+
const absPath = path.resolve(entryPoint);
|
|
60
96
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
if (!fs.existsSync(absPath)) {
|
|
98
|
+
throw new BundleError(
|
|
99
|
+
`Entry point does not exist: ${entryPoint}`,
|
|
100
|
+
[{ text: `Cannot find file: ${absPath}`, location: { file: entryPoint } }]
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await fs.promises.access(absPath, fs.constants.R_OK);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new BundleError(
|
|
108
|
+
`Entry point is not readable: ${entryPoint}`,
|
|
109
|
+
[{ text: `Cannot read file: ${absPath}`, location: { file: entryPoint } }]
|
|
110
|
+
);
|
|
111
|
+
}
|
|
72
112
|
}
|
|
73
113
|
|
|
74
114
|
/**
|
|
75
|
-
* Bundles a single
|
|
76
|
-
* @param {Object} options - Bundle options
|
|
77
|
-
* @returns {Promise<void>}
|
|
78
|
-
* @throws {BundleError} If bundling fails
|
|
115
|
+
* Bundles a single file
|
|
79
116
|
*/
|
|
80
117
|
export async function bundleFile(options) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Validate entry point exists
|
|
95
|
-
await validateEntryPoint(entryPoint);
|
|
96
|
-
|
|
97
|
-
// Ensure output directory exists
|
|
98
|
-
const outDir = path.dirname(outfile);
|
|
99
|
-
await fs.promises.mkdir(outDir, { recursive: true });
|
|
118
|
+
const {
|
|
119
|
+
entryPoint,
|
|
120
|
+
outfile,
|
|
121
|
+
format = 'iife',
|
|
122
|
+
minify = false,
|
|
123
|
+
sourcemap = false,
|
|
124
|
+
platform = 'neutral',
|
|
125
|
+
globalName = '__titan_exports',
|
|
126
|
+
target = 'es2020',
|
|
127
|
+
banner = {},
|
|
128
|
+
footer = {}
|
|
129
|
+
} = options;
|
|
100
130
|
|
|
101
|
-
|
|
102
|
-
// Run esbuild with error logging enabled
|
|
103
|
-
const result = await esbuild.build({
|
|
104
|
-
entryPoints: [entryPoint],
|
|
105
|
-
bundle: true,
|
|
106
|
-
outfile,
|
|
107
|
-
format,
|
|
108
|
-
globalName,
|
|
109
|
-
platform,
|
|
110
|
-
target,
|
|
111
|
-
banner,
|
|
112
|
-
footer,
|
|
113
|
-
minify,
|
|
114
|
-
sourcemap,
|
|
115
|
-
logLevel: 'silent', // We handle all errors ourselves
|
|
116
|
-
logLimit: 0,
|
|
117
|
-
color: false,
|
|
118
|
-
write: true,
|
|
119
|
-
metafile: false,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Check for errors in the result
|
|
123
|
-
if (result.errors && result.errors.length > 0) {
|
|
124
|
-
throw new BundleError(
|
|
125
|
-
`Build failed with ${result.errors.length} error(s)`,
|
|
126
|
-
result.errors,
|
|
127
|
-
result.warnings || []
|
|
128
|
-
);
|
|
129
|
-
}
|
|
131
|
+
await validateEntryPoint(entryPoint);
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
const outDir = path.dirname(outfile);
|
|
134
|
+
await fs.promises.mkdir(outDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = await esbuild.build({
|
|
138
|
+
entryPoints: [entryPoint],
|
|
139
|
+
bundle: true,
|
|
140
|
+
outfile,
|
|
141
|
+
format,
|
|
142
|
+
globalName,
|
|
143
|
+
platform,
|
|
144
|
+
target,
|
|
145
|
+
banner,
|
|
146
|
+
footer,
|
|
147
|
+
minify,
|
|
148
|
+
sourcemap,
|
|
149
|
+
logLevel: 'silent',
|
|
150
|
+
logLimit: 0,
|
|
151
|
+
write: true,
|
|
152
|
+
metafile: false,
|
|
153
|
+
plugins: [titanNodeCompatPlugin],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (result.errors?.length) {
|
|
157
|
+
throw new BundleError(
|
|
158
|
+
`Build failed with ${result.errors.length} error(s)`,
|
|
159
|
+
result.errors,
|
|
160
|
+
result.warnings || []
|
|
161
|
+
);
|
|
162
|
+
}
|
|
140
163
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.errors?.length) {
|
|
166
|
+
throw new BundleError(
|
|
167
|
+
`Build failed with ${err.errors.length} error(s)`,
|
|
168
|
+
err.errors,
|
|
169
|
+
err.warnings || []
|
|
170
|
+
);
|
|
149
171
|
}
|
|
172
|
+
|
|
173
|
+
throw new BundleError(
|
|
174
|
+
`Unexpected build error: ${err.message}`,
|
|
175
|
+
[{ text: err.message, location: { file: entryPoint } }]
|
|
176
|
+
);
|
|
177
|
+
}
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
/**
|
|
153
|
-
* Main
|
|
154
|
-
* RULE: This function handles ALL esbuild errors and prints error boxes directly
|
|
155
|
-
* RULE: After printing error box, throws Error("__TITAN_BUNDLE_FAILED__")
|
|
156
|
-
* @returns {Promise<void>}
|
|
181
|
+
* Main bundler
|
|
157
182
|
*/
|
|
158
183
|
export async function bundle() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
184
|
+
const root = process.cwd();
|
|
185
|
+
const actionsDir = path.join(root, 'app', 'actions');
|
|
186
|
+
const bundleDir = path.join(root, 'server', 'src', 'actions');
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
await fs.promises.mkdir(bundleDir, { recursive: true });
|
|
168
|
-
|
|
169
|
-
// Check if actions directory exists
|
|
170
|
-
if (!fs.existsSync(actionsDir)) {
|
|
171
|
-
return; // No actions to bundle
|
|
172
|
-
}
|
|
188
|
+
if (fs.existsSync(bundleDir)) {
|
|
189
|
+
fs.rmSync(bundleDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
await fs.promises.mkdir(bundleDir, { recursive: true });
|
|
173
192
|
|
|
174
|
-
|
|
175
|
-
const files = fs.readdirSync(actionsDir).filter(f =>
|
|
176
|
-
(f.endsWith('.js') || f.endsWith('.ts')) && !f.endsWith('.d.ts')
|
|
177
|
-
);
|
|
193
|
+
if (!fs.existsSync(actionsDir)) return;
|
|
178
194
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
const files = fs.readdirSync(actionsDir).filter(f =>
|
|
196
|
+
(f.endsWith('.js') || f.endsWith('.ts')) && !f.endsWith('.d.ts')
|
|
197
|
+
);
|
|
182
198
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const outfile = path.join(bundleDir, actionName + ".jsbundle");
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
const actionName = path.basename(file, path.extname(file));
|
|
201
|
+
const entryPoint = path.join(actionsDir, file);
|
|
202
|
+
const outfile = path.join(bundleDir, actionName + ".jsbundle");
|
|
188
203
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
js: "var Titan = t;"
|
|
201
|
-
},
|
|
202
|
-
footer: {
|
|
203
|
-
js: `
|
|
204
|
+
try {
|
|
205
|
+
await bundleFile({
|
|
206
|
+
entryPoint,
|
|
207
|
+
outfile,
|
|
208
|
+
format: 'iife',
|
|
209
|
+
globalName: '__titan_exports',
|
|
210
|
+
platform: 'node',
|
|
211
|
+
target: 'es2020',
|
|
212
|
+
banner: { js: "var Titan = t;" },
|
|
213
|
+
footer: {
|
|
214
|
+
js: `
|
|
204
215
|
(function () {
|
|
205
216
|
const fn =
|
|
206
217
|
__titan_exports["${actionName}"] ||
|
|
@@ -213,52 +224,36 @@ export async function bundle() {
|
|
|
213
224
|
globalThis["${actionName}"] = globalThis.defineAction(fn);
|
|
214
225
|
})();
|
|
215
226
|
`
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
} catch (error) {
|
|
219
|
-
// RULE: Handle esbuild errors HERE and print error boxes
|
|
220
|
-
if (error.isBundleError && error.errors && error.errors.length > 0) {
|
|
221
|
-
// Print error box for each esbuild error
|
|
222
|
-
console.error(); // Empty line for spacing
|
|
223
|
-
|
|
224
|
-
const titanVersion = getTitanVersion();
|
|
225
|
-
|
|
226
|
-
for (let i = 0; i < error.errors.length; i++) {
|
|
227
|
-
const esbuildError = error.errors[i];
|
|
228
|
-
const errorInfo = parseEsbuildError(esbuildError);
|
|
229
|
-
|
|
230
|
-
// Add error number to title if multiple errors
|
|
231
|
-
if (error.errors.length > 1) {
|
|
232
|
-
errorInfo.title = `Build Error ${i + 1}/${error.errors.length}`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Add Titan version
|
|
236
|
-
errorInfo.titanVersion = titanVersion;
|
|
237
|
-
|
|
238
|
-
// Print the error box
|
|
239
|
-
console.error(renderErrorBox(errorInfo));
|
|
240
|
-
|
|
241
|
-
if (i < error.errors.length - 1) {
|
|
242
|
-
console.error(); // Empty line between errors
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
console.error(); // Empty line after all errors
|
|
247
|
-
} else {
|
|
248
|
-
// Other errors
|
|
249
|
-
console.error();
|
|
250
|
-
const errorInfo = {
|
|
251
|
-
title: 'Build Error',
|
|
252
|
-
file: entryPoint,
|
|
253
|
-
message: error.message || 'Unknown error',
|
|
254
|
-
titanVersion: getTitanVersion()
|
|
255
|
-
};
|
|
256
|
-
console.error(renderErrorBox(errorInfo));
|
|
257
|
-
console.error();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// RULE: Throw special error to signal bundle failure
|
|
261
|
-
throw new Error('__TITAN_BUNDLE_FAILED__');
|
|
262
227
|
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
|
|
232
|
+
console.error();
|
|
233
|
+
|
|
234
|
+
const titanVersion = getTitanVersion();
|
|
235
|
+
|
|
236
|
+
if (error.isBundleError && error.errors?.length) {
|
|
237
|
+
for (let i = 0; i < error.errors.length; i++) {
|
|
238
|
+
const errorInfo = parseEsbuildError(error.errors[i]);
|
|
239
|
+
if (error.errors.length > 1) {
|
|
240
|
+
errorInfo.title = `Build Error ${i + 1}/${error.errors.length}`;
|
|
241
|
+
}
|
|
242
|
+
errorInfo.titanVersion = titanVersion;
|
|
243
|
+
console.error(renderErrorBox(errorInfo));
|
|
244
|
+
console.error();
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
const errorInfo = {
|
|
248
|
+
title: 'Build Error',
|
|
249
|
+
file: entryPoint,
|
|
250
|
+
message: error.message || 'Unknown error',
|
|
251
|
+
titanVersion
|
|
252
|
+
};
|
|
253
|
+
console.error(renderErrorBox(errorInfo));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new Error('__TITAN_BUNDLE_FAILED__');
|
|
263
257
|
}
|
|
264
|
-
}
|
|
258
|
+
}
|
|
259
|
+
}
|