react-bun-ssr 0.1.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.
@@ -0,0 +1,115 @@
1
+ import type { DeferredLoaderResult, DeferredToken } from "./types";
2
+
3
+ interface ThenableLike {
4
+ then: (onFulfilled?: (value: unknown) => unknown, onRejected?: (reason: unknown) => unknown) => unknown;
5
+ }
6
+
7
+ export interface DeferredSettleEntry {
8
+ id: string;
9
+ settled: Promise<
10
+ | { ok: true; value: unknown }
11
+ | { ok: false; error: string }
12
+ >;
13
+ }
14
+
15
+ export interface PreparedDeferredPayload {
16
+ dataForRender: Record<string, unknown>;
17
+ dataForPayload: Record<string, unknown>;
18
+ settleEntries: DeferredSettleEntry[];
19
+ }
20
+
21
+ const DEFERRED_TYPE = "defer";
22
+ const DEFERRED_TOKEN_KEY = "__rbssrDeferred";
23
+
24
+ let deferredCounter = 0;
25
+
26
+ function nextDeferredId(routeId: string, key: string): string {
27
+ deferredCounter += 1;
28
+ return `${routeId}:${key}:${deferredCounter}`;
29
+ }
30
+
31
+ function isThenable(value: unknown): value is ThenableLike {
32
+ return Boolean(
33
+ value &&
34
+ (typeof value === "object" || typeof value === "function") &&
35
+ typeof (value as ThenableLike).then === "function",
36
+ );
37
+ }
38
+
39
+ function toErrorMessage(error: unknown): string {
40
+ if (error instanceof Error) {
41
+ return error.message;
42
+ }
43
+ return String(error);
44
+ }
45
+
46
+ export function defer<T extends Record<string, unknown>>(data: T): DeferredLoaderResult<T> {
47
+ if (!data || Array.isArray(data) || typeof data !== "object") {
48
+ throw new Error("defer() expects an object with top-level keys.");
49
+ }
50
+
51
+ return {
52
+ __rbssrType: DEFERRED_TYPE,
53
+ data,
54
+ };
55
+ }
56
+
57
+ export function isDeferredLoaderResult(value: unknown): value is DeferredLoaderResult<Record<string, unknown>> {
58
+ return Boolean(
59
+ value &&
60
+ typeof value === "object" &&
61
+ (value as DeferredLoaderResult<Record<string, unknown>>).__rbssrType === DEFERRED_TYPE &&
62
+ (value as DeferredLoaderResult<Record<string, unknown>>).data &&
63
+ !Array.isArray((value as DeferredLoaderResult<Record<string, unknown>>).data) &&
64
+ typeof (value as DeferredLoaderResult<Record<string, unknown>>).data === "object",
65
+ );
66
+ }
67
+
68
+ export function isDeferredToken(value: unknown): value is DeferredToken {
69
+ return Boolean(
70
+ value &&
71
+ typeof value === "object" &&
72
+ typeof (value as DeferredToken)[DEFERRED_TOKEN_KEY] === "string",
73
+ );
74
+ }
75
+
76
+ export function createDeferredToken(id: string): DeferredToken {
77
+ return {
78
+ [DEFERRED_TOKEN_KEY]: id,
79
+ };
80
+ }
81
+
82
+ export function prepareDeferredPayload(
83
+ routeId: string,
84
+ deferredValue: DeferredLoaderResult<Record<string, unknown>>,
85
+ ): PreparedDeferredPayload {
86
+ const dataForRender: Record<string, unknown> = {};
87
+ const dataForPayload: Record<string, unknown> = {};
88
+ const settleEntries: DeferredSettleEntry[] = [];
89
+
90
+ for (const [key, value] of Object.entries(deferredValue.data)) {
91
+ if (!isThenable(value)) {
92
+ dataForRender[key] = value;
93
+ dataForPayload[key] = value;
94
+ continue;
95
+ }
96
+
97
+ const id = nextDeferredId(routeId, key);
98
+ const promise = Promise.resolve(value);
99
+ dataForRender[key] = promise;
100
+ dataForPayload[key] = createDeferredToken(id);
101
+ settleEntries.push({
102
+ id,
103
+ settled: promise.then(
104
+ resolved => ({ ok: true as const, value: resolved }),
105
+ error => ({ ok: false as const, error: toErrorMessage(error) }),
106
+ ),
107
+ });
108
+ }
109
+
110
+ return {
111
+ dataForRender,
112
+ dataForPayload,
113
+ settleEntries,
114
+ };
115
+ }
@@ -0,0 +1,40 @@
1
+ import type { FrameworkConfig, RedirectResult } from "./types";
2
+ import { defer as deferValue } from "./deferred";
3
+
4
+ export function json(data: unknown, init: ResponseInit = {}): Response {
5
+ const headers = new Headers(init.headers);
6
+ if (!headers.has("content-type")) {
7
+ headers.set("content-type", "application/json; charset=utf-8");
8
+ }
9
+
10
+ return new Response(JSON.stringify(data), {
11
+ ...init,
12
+ headers,
13
+ });
14
+ }
15
+
16
+ export function redirect(
17
+ location: string,
18
+ status: RedirectResult["status"] = 302,
19
+ ): RedirectResult {
20
+ return {
21
+ type: "redirect",
22
+ location,
23
+ status,
24
+ };
25
+ }
26
+
27
+ export function defineConfig(config: FrameworkConfig): FrameworkConfig {
28
+ return config;
29
+ }
30
+
31
+ export const defer = deferValue;
32
+
33
+ export function isRedirectResult(value: unknown): value is RedirectResult {
34
+ return Boolean(
35
+ value &&
36
+ typeof value === "object" &&
37
+ (value as RedirectResult).type === "redirect" &&
38
+ typeof (value as RedirectResult).location === "string",
39
+ );
40
+ }
@@ -0,0 +1,24 @@
1
+ export type {
2
+ Action,
3
+ ActionContext,
4
+ ActionResult,
5
+ ApiRouteModule,
6
+ BuildManifest,
7
+ BuildRouteAsset,
8
+ DeferredLoaderResult,
9
+ DeferredToken,
10
+ FrameworkConfig,
11
+ Loader,
12
+ LoaderContext,
13
+ LoaderResult,
14
+ Middleware,
15
+ Params,
16
+ RedirectResult,
17
+ ResponseHeaderRule,
18
+ RequestContext,
19
+ RouteModule,
20
+ } from "./types";
21
+
22
+ export { createServer, startHttpServer } from "./server";
23
+ export { defer, json, redirect, defineConfig } from "./helpers";
24
+ export { Outlet, useLoaderData, useParams, useRequestUrl, useRouteError } from "./tree";
@@ -0,0 +1,146 @@
1
+ import path from "node:path";
2
+
3
+ type BunFileStat = Awaited<ReturnType<ReturnType<typeof Bun.file>["stat"]>>;
4
+ export type HashInput = string | ArrayBuffer | Uint8Array;
5
+ export interface FileEntry {
6
+ name: string;
7
+ isDirectory: boolean;
8
+ isFile: boolean;
9
+ }
10
+
11
+ function isErrno(error: unknown, code: string): boolean {
12
+ return Boolean(
13
+ error
14
+ && typeof error === "object"
15
+ && "code" in error
16
+ && (error as { code?: unknown }).code === code,
17
+ );
18
+ }
19
+
20
+ export async function statPath(filePath: string): Promise<BunFileStat | null> {
21
+ try {
22
+ return await Bun.file(filePath).stat();
23
+ } catch (error) {
24
+ if (isErrno(error, "ENOENT")) {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ export async function existsPath(filePath: string): Promise<boolean> {
32
+ return (await statPath(filePath)) !== null;
33
+ }
34
+
35
+ export async function readText(filePath: string): Promise<string> {
36
+ return Bun.file(filePath).text();
37
+ }
38
+
39
+ export async function writeText(filePath: string, content: string): Promise<void> {
40
+ await ensureDir(path.dirname(filePath));
41
+ await Bun.write(filePath, content);
42
+ }
43
+
44
+ export async function writeTextIfChanged(filePath: string, content: string): Promise<boolean> {
45
+ const stat = await statPath(filePath);
46
+ if (stat?.isFile()) {
47
+ const current = await readText(filePath);
48
+ if (current === content) {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ await writeText(filePath, content);
54
+ return true;
55
+ }
56
+
57
+ export async function glob(
58
+ pattern: string,
59
+ options: {
60
+ cwd: string;
61
+ absolute?: boolean;
62
+ dot?: boolean;
63
+ },
64
+ ): Promise<string[]> {
65
+ const entries: string[] = [];
66
+ const scanner = new Bun.Glob(pattern);
67
+ for await (const entry of scanner.scan({
68
+ ...options,
69
+ dot: options.dot ?? true,
70
+ })) {
71
+ entries.push(entry);
72
+ }
73
+ return entries.sort((a, b) => a.localeCompare(b));
74
+ }
75
+
76
+ export function sha256Short(input: HashInput): string {
77
+ const hasher = new Bun.CryptoHasher("sha256");
78
+ hasher.update(input);
79
+ return hasher.digest("hex").slice(0, 8);
80
+ }
81
+
82
+ export async function ensureDir(dirPath: string): Promise<void> {
83
+ runPosix(["mkdir", "-p", dirPath], `Failed to create directory: ${dirPath}`);
84
+ }
85
+
86
+ export async function ensureCleanDir(dirPath: string): Promise<void> {
87
+ await removePath(dirPath);
88
+ await ensureDir(dirPath);
89
+ }
90
+
91
+ export async function removePath(targetPath: string): Promise<void> {
92
+ runPosix(["rm", "-rf", targetPath], `Failed to remove path: ${targetPath}`);
93
+ }
94
+
95
+ export async function listEntries(dirPath: string): Promise<FileEntry[]> {
96
+ const dirStat = await statPath(dirPath);
97
+ if (!dirStat?.isDirectory()) {
98
+ return [];
99
+ }
100
+
101
+ const names = await glob("*", {
102
+ cwd: dirPath,
103
+ dot: true,
104
+ });
105
+
106
+ const entries: FileEntry[] = [];
107
+ for (const name of names) {
108
+ const absolutePath = path.join(dirPath, name);
109
+ const entryStat = await statPath(absolutePath);
110
+ if (!entryStat) {
111
+ continue;
112
+ }
113
+
114
+ entries.push({
115
+ name,
116
+ isDirectory: entryStat.isDirectory(),
117
+ isFile: entryStat.isFile(),
118
+ });
119
+ }
120
+
121
+ return entries;
122
+ }
123
+
124
+ export async function makeTempDir(prefix: string): Promise<string> {
125
+ const dirPath = path.join("/tmp", `${prefix}-${crypto.randomUUID()}`);
126
+ await ensureDir(dirPath);
127
+ return dirPath;
128
+ }
129
+
130
+ function runPosix(cmd: string[], context: string): void {
131
+ const result = Bun.spawnSync({
132
+ cmd,
133
+ stdout: "pipe",
134
+ stderr: "pipe",
135
+ });
136
+
137
+ if (result.exitCode === 0) {
138
+ return;
139
+ }
140
+
141
+ const decoder = new TextDecoder();
142
+ const stderr = result.stderr.length > 0 ? decoder.decode(result.stderr).trim() : "";
143
+ const stdout = result.stdout.length > 0 ? decoder.decode(result.stdout).trim() : "";
144
+ const details = stderr || stdout || `exit code ${result.exitCode}`;
145
+ throw new Error(`[io] ${context} (${cmd.join(" ")}): ${details}`);
146
+ }
@@ -0,0 +1,319 @@
1
+ import path from "node:path";
2
+ import { existsPath, readText, writeTextIfChanged } from "./io";
3
+ import { normalizeSlashes, stableHash, trimFileExtension } from "./utils";
4
+
5
+ const compiledMarkdownCache = new Map<string, { sourceHash: string; outputPath: string }>();
6
+ const REQUIRED_FRONTMATTER_FIELDS = ["title", "description", "section", "order"] as const;
7
+ const MARKDOWN_WRAPPER_VERSION = "2";
8
+
9
+ interface ParsedFrontmatter {
10
+ title?: string;
11
+ description?: string;
12
+ section?: string;
13
+ tags: string[];
14
+ }
15
+
16
+ function decodeHtml(value: string): string {
17
+ return value
18
+ .replace(/&lt;/g, "<")
19
+ .replace(/&gt;/g, ">")
20
+ .replace(/&quot;/g, "\"")
21
+ .replace(/&#39;/g, "'")
22
+ .replace(/&amp;/g, "&");
23
+ }
24
+
25
+ function escapeHtml(value: string): string {
26
+ return value
27
+ .replace(/&/g, "&amp;")
28
+ .replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;")
30
+ .replace(/\"/g, "&quot;")
31
+ .replace(/'/g, "&#39;");
32
+ }
33
+
34
+ function highlightWithRegex(
35
+ source: string,
36
+ regex: RegExp,
37
+ classify: (value: string) => string,
38
+ ): string {
39
+ let cursor = 0;
40
+ let html = "";
41
+ let match = regex.exec(source);
42
+
43
+ while (match) {
44
+ const value = match[0] ?? "";
45
+ const index = match.index;
46
+
47
+ if (index > cursor) {
48
+ html += escapeHtml(source.slice(cursor, index));
49
+ }
50
+
51
+ html += `<span class="token ${classify(value)}">${escapeHtml(value)}</span>`;
52
+ cursor = index + value.length;
53
+ match = regex.exec(source);
54
+ }
55
+
56
+ if (cursor < source.length) {
57
+ html += escapeHtml(source.slice(cursor));
58
+ }
59
+
60
+ return html;
61
+ }
62
+
63
+ function highlightCode(source: string, language: string): string {
64
+ const normalized = language.toLowerCase();
65
+
66
+ if (["ts", "tsx", "js", "jsx", "mjs", "cjs"].includes(normalized)) {
67
+ const pattern =
68
+ /(\/\/.*$|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|import|from|export|default|async|await|try|catch|throw|new|class|extends|interface|type|implements|public|private|protected|readonly|as|in|of|typeof)\b|\b(?:true|false|null|undefined)\b|\b\d+(?:\.\d+)?\b)/gm;
69
+
70
+ return highlightWithRegex(source, pattern, value => {
71
+ if (value.startsWith("//") || value.startsWith("/*")) return "comment";
72
+ if (
73
+ value.startsWith("\"") ||
74
+ value.startsWith("'") ||
75
+ value.startsWith("`")
76
+ )
77
+ return "string";
78
+ if (/^\d/.test(value)) return "number";
79
+ if (/^(true|false|null|undefined)$/.test(value)) return "constant";
80
+ return "keyword";
81
+ });
82
+ }
83
+
84
+ if (["bash", "sh", "zsh", "shell"].includes(normalized)) {
85
+ const pattern =
86
+ /(#.*$|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:if|then|fi|for|in|do|done|case|esac|while|function|export)\b|(?:^|\s)(?:bun|npm|node|git|curl|cd|ls|cat|echo)(?=\s|$)|--?[a-zA-Z0-9-]+)/gm;
87
+
88
+ return highlightWithRegex(source, pattern, value => {
89
+ const trimmed = value.trim();
90
+ if (trimmed.startsWith("#")) return "comment";
91
+ if (trimmed.startsWith("\"") || trimmed.startsWith("'")) return "string";
92
+ if (trimmed.startsWith("-")) return "operator";
93
+ if (/^(if|then|fi|for|in|do|done|case|esac|while|function|export)$/.test(trimmed))
94
+ return "keyword";
95
+ return "builtin";
96
+ });
97
+ }
98
+
99
+ return escapeHtml(source);
100
+ }
101
+
102
+ function applySyntaxHighlight(html: string): string {
103
+ return html.replace(
104
+ /<pre><code class="language-([^"]+)">([\s\S]*?)<\/code><\/pre>/g,
105
+ (_match, language: string, rawCode: string) => {
106
+ const code = decodeHtml(rawCode);
107
+ const highlighted = highlightCode(code, language);
108
+ return `<pre><code class="language-${escapeHtml(language)}">${highlighted}</code></pre>`;
109
+ },
110
+ );
111
+ }
112
+
113
+ function resolveGeneratedRoot(routesDir: string, generatedMarkdownRootDir?: string): string {
114
+ if (generatedMarkdownRootDir) {
115
+ return path.resolve(generatedMarkdownRootDir);
116
+ }
117
+
118
+ const normalizedRoutesDir = normalizeSlashes(path.resolve(routesDir));
119
+ const appRoutesMatch = normalizedRoutesDir.match(/^(.*)\/app\/routes$/);
120
+ if (appRoutesMatch) {
121
+ return path.resolve(appRoutesMatch[1]!, ".rbssr", "generated", "markdown-routes");
122
+ }
123
+
124
+ const snapshotRoutesMatch = normalizedRoutesDir.match(
125
+ /^(.*)\/\.rbssr\/dev\/server-snapshots\/v\d+\/routes$/,
126
+ );
127
+ if (snapshotRoutesMatch) {
128
+ return path.resolve(snapshotRoutesMatch[1]!, ".rbssr", "generated", "markdown-routes");
129
+ }
130
+
131
+ return path.resolve(routesDir, "..", ".rbssr", "generated", "markdown-routes");
132
+ }
133
+
134
+ function toRouteGroupKey(routesDir: string): string {
135
+ const normalized = normalizeSlashes(path.resolve(routesDir));
136
+ const canonical = normalized.replace(
137
+ /\/\.rbssr\/dev\/server-snapshots\/v\d+\/routes$/,
138
+ "/.rbssr/dev/server-snapshots/routes",
139
+ );
140
+
141
+ return stableHash(`${MARKDOWN_WRAPPER_VERSION}\0${canonical}`);
142
+ }
143
+
144
+ function parseFrontmatter(raw: string): {
145
+ frontmatter: ParsedFrontmatter;
146
+ markdown: string;
147
+ } {
148
+ if (!raw.startsWith("---\n")) {
149
+ return {
150
+ frontmatter: { tags: [] },
151
+ markdown: raw,
152
+ };
153
+ }
154
+
155
+ const end = raw.indexOf("\n---\n", 4);
156
+ if (end < 0) {
157
+ return {
158
+ frontmatter: { tags: [] },
159
+ markdown: raw,
160
+ };
161
+ }
162
+
163
+ const rawFrontmatterLines = raw.slice(4, end).split("\n");
164
+ const values = new Map<string, string>();
165
+ for (const line of rawFrontmatterLines) {
166
+ const separator = line.indexOf(":");
167
+ if (separator < 0) {
168
+ continue;
169
+ }
170
+ const key = line.slice(0, separator).trim();
171
+ const value = line.slice(separator + 1).trim();
172
+ values.set(key, value);
173
+ }
174
+
175
+ for (const key of REQUIRED_FRONTMATTER_FIELDS) {
176
+ if (!values.has(key)) {
177
+ return {
178
+ frontmatter: { tags: [] },
179
+ markdown: raw,
180
+ };
181
+ }
182
+ }
183
+
184
+ const tags = (values.get("tags") ?? "")
185
+ .split(",")
186
+ .map(value => value.trim())
187
+ .filter(Boolean);
188
+
189
+ return {
190
+ frontmatter: {
191
+ title: values.get("title"),
192
+ description: values.get("description"),
193
+ section: values.get("section"),
194
+ tags,
195
+ },
196
+ markdown: raw.slice(end + 5),
197
+ };
198
+ }
199
+
200
+ function stripLeadingH1(html: string): string {
201
+ return html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/i, "");
202
+ }
203
+
204
+ function toWrapperSource(options: {
205
+ html: string;
206
+ frontmatter: ParsedFrontmatter;
207
+ }): string {
208
+ const { html, frontmatter } = options;
209
+ const title = frontmatter.title ?? "";
210
+ const description = frontmatter.description ?? "";
211
+ const section = frontmatter.section ?? "";
212
+ const tags = frontmatter.tags ?? [];
213
+
214
+ return `const markdownHtml = ${JSON.stringify(html)};
215
+ const markdownTitle = ${JSON.stringify(title)};
216
+ const markdownDescription = ${JSON.stringify(description)};
217
+ const markdownSection = ${JSON.stringify(section)};
218
+ const markdownTags = ${JSON.stringify(tags)};
219
+
220
+ export default function MarkdownRoute() {
221
+ return (
222
+ <>
223
+ {markdownTitle ? (
224
+ <header className="docs-hero">
225
+ {markdownSection ? <p className="kicker">{markdownSection}</p> : null}
226
+ <h1>{markdownTitle}</h1>
227
+ {markdownDescription ? <p>{markdownDescription}</p> : null}
228
+ {markdownTags.length > 0 ? (
229
+ <div>
230
+ {markdownTags.map(tag => (
231
+ <span key={tag}>{tag}</span>
232
+ ))}
233
+ </div>
234
+ ) : null}
235
+ </header>
236
+ ) : null}
237
+ <section className="docs-content-body" dangerouslySetInnerHTML={{ __html: markdownHtml }} />
238
+ </>
239
+ );
240
+ }
241
+
242
+ export function head() {
243
+ if (!markdownTitle) {
244
+ return null;
245
+ }
246
+ return <title>{markdownTitle}</title>;
247
+ }
248
+
249
+ export function meta() {
250
+ const values: Record<string, string> = {};
251
+
252
+ if (markdownDescription) {
253
+ values.description = markdownDescription;
254
+ values["og:description"] = markdownDescription;
255
+ values["twitter:description"] = markdownDescription;
256
+ }
257
+
258
+ if (markdownTitle) {
259
+ values["og:title"] = markdownTitle;
260
+ values["twitter:title"] = markdownTitle;
261
+ }
262
+
263
+ values["og:type"] = "article";
264
+ values["twitter:card"] = "summary_large_image";
265
+
266
+ if (markdownSection) {
267
+ values["article:section"] = markdownSection;
268
+ }
269
+
270
+ if (markdownTags.length > 0) {
271
+ const joinedTags = markdownTags.join(", ");
272
+ values.keywords = joinedTags;
273
+ values["article:tag"] = joinedTags;
274
+ }
275
+
276
+ return values;
277
+ }
278
+ `;
279
+ }
280
+
281
+ async function writeFileIfChanged(filePath: string, content: string): Promise<void> {
282
+ await writeTextIfChanged(filePath, content);
283
+ }
284
+
285
+ export async function compileMarkdownRouteModule(options: {
286
+ routesDir: string;
287
+ sourceFilePath: string;
288
+ generatedMarkdownRootDir?: string;
289
+ }): Promise<string> {
290
+ const routesDir = path.resolve(options.routesDir);
291
+ const sourceFilePath = path.resolve(options.sourceFilePath);
292
+ const generatedRoot = resolveGeneratedRoot(routesDir, options.generatedMarkdownRootDir);
293
+ const routeGroupKey = toRouteGroupKey(routesDir);
294
+ const relativeRoutePath = normalizeSlashes(path.relative(routesDir, sourceFilePath));
295
+ const routeModuleRelativePath = `${trimFileExtension(relativeRoutePath)}.tsx`;
296
+ const outputPath = path.join(generatedRoot, routeGroupKey, routeModuleRelativePath);
297
+
298
+ const markdownSource = await readText(sourceFilePath);
299
+ const sourceHash = stableHash(`${MARKDOWN_WRAPPER_VERSION}\0${markdownSource}`);
300
+ const cacheKey = `${sourceFilePath}::${outputPath}`;
301
+ const cached = compiledMarkdownCache.get(cacheKey);
302
+ if (cached && cached.sourceHash === sourceHash && await existsPath(cached.outputPath)) {
303
+ return cached.outputPath;
304
+ }
305
+
306
+ const parsed = parseFrontmatter(markdownSource);
307
+ const highlightedHtml = applySyntaxHighlight(Bun.markdown.html(parsed.markdown));
308
+ const html = parsed.frontmatter.title ? stripLeadingH1(highlightedHtml) : highlightedHtml;
309
+ await writeFileIfChanged(
310
+ outputPath,
311
+ toWrapperSource({
312
+ html,
313
+ frontmatter: parsed.frontmatter,
314
+ }),
315
+ );
316
+ compiledMarkdownCache.set(cacheKey, { sourceHash, outputPath });
317
+
318
+ return outputPath;
319
+ }