@teyik0/furin 0.1.0-alpha.3

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.
Files changed (71) hide show
  1. package/dist/adapter/bun.d.ts +3 -0
  2. package/dist/build/client.d.ts +14 -0
  3. package/dist/build/compile-entry.d.ts +22 -0
  4. package/dist/build/entry-template.d.ts +13 -0
  5. package/dist/build/hydrate.d.ts +20 -0
  6. package/dist/build/index.d.ts +7 -0
  7. package/dist/build/index.js +2212 -0
  8. package/dist/build/route-types.d.ts +20 -0
  9. package/dist/build/scan-server.d.ts +8 -0
  10. package/dist/build/server-routes-entry.d.ts +22 -0
  11. package/dist/build/shared.d.ts +12 -0
  12. package/dist/build/types.d.ts +53 -0
  13. package/dist/cli/config.d.ts +9 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +2240 -0
  16. package/dist/client.d.ts +158 -0
  17. package/dist/client.js +20 -0
  18. package/dist/config.d.ts +16 -0
  19. package/dist/config.js +23 -0
  20. package/dist/furin.d.ts +45 -0
  21. package/dist/furin.js +937 -0
  22. package/dist/internal.d.ts +18 -0
  23. package/dist/link.d.ts +119 -0
  24. package/dist/link.js +281 -0
  25. package/dist/plugin/index.d.ts +20 -0
  26. package/dist/plugin/index.js +1408 -0
  27. package/dist/plugin/transform-client.d.ts +9 -0
  28. package/dist/render/assemble.d.ts +13 -0
  29. package/dist/render/cache.d.ts +7 -0
  30. package/dist/render/element.d.ts +4 -0
  31. package/dist/render/index.d.ts +26 -0
  32. package/dist/render/loaders.d.ts +12 -0
  33. package/dist/render/shell.d.ts +17 -0
  34. package/dist/render/template.d.ts +4 -0
  35. package/dist/router.d.ts +32 -0
  36. package/dist/router.js +575 -0
  37. package/dist/runtime-env.d.ts +3 -0
  38. package/dist/tsconfig.dts.tsbuildinfo +1 -0
  39. package/dist/utils.d.ts +6 -0
  40. package/package.json +74 -0
  41. package/src/adapter/README.md +13 -0
  42. package/src/adapter/bun.ts +119 -0
  43. package/src/build/client.ts +110 -0
  44. package/src/build/compile-entry.ts +99 -0
  45. package/src/build/entry-template.ts +62 -0
  46. package/src/build/hydrate.ts +106 -0
  47. package/src/build/index.ts +120 -0
  48. package/src/build/route-types.ts +88 -0
  49. package/src/build/scan-server.ts +88 -0
  50. package/src/build/server-routes-entry.ts +38 -0
  51. package/src/build/shared.ts +80 -0
  52. package/src/build/types.ts +60 -0
  53. package/src/cli/config.ts +68 -0
  54. package/src/cli/index.ts +106 -0
  55. package/src/client.ts +237 -0
  56. package/src/config.ts +31 -0
  57. package/src/furin.ts +251 -0
  58. package/src/internal.ts +36 -0
  59. package/src/link.tsx +480 -0
  60. package/src/plugin/index.ts +80 -0
  61. package/src/plugin/transform-client.ts +372 -0
  62. package/src/render/assemble.ts +57 -0
  63. package/src/render/cache.ts +9 -0
  64. package/src/render/element.tsx +28 -0
  65. package/src/render/index.ts +312 -0
  66. package/src/render/loaders.ts +67 -0
  67. package/src/render/shell.ts +128 -0
  68. package/src/render/template.ts +54 -0
  69. package/src/router.ts +234 -0
  70. package/src/runtime-env.ts +6 -0
  71. package/src/utils.ts +68 -0
@@ -0,0 +1,312 @@
1
+ import { renderToReadableStream } from "react-dom/server";
2
+ import type { RootLayout } from "../router.ts";
3
+ import { assembleHTML, resolvePath, splitTemplate, streamToString } from "./assemble.ts";
4
+ import { isrCache, ssgCache } from "./cache.ts";
5
+ import { buildElement } from "./element.tsx";
6
+ import { runLoaders } from "./loaders.ts";
7
+ import { buildHeadInjection, safeJson } from "./shell.ts";
8
+ import { getDevTemplate, getProductionTemplate } from "./template.ts";
9
+
10
+ // ── Re-exports (public API) ──────────────────────────────────────────────────
11
+ // biome-ignore lint/performance/noBarrelFile: acnowledged
12
+ export { type LoaderContext, streamToString } from "./assemble.ts";
13
+ export { buildElement } from "./element.tsx";
14
+ export { type LoaderResult, runLoaders } from "./loaders.ts";
15
+
16
+ // ── Types ────────────────────────────────────────────────────────────────────
17
+
18
+ import type { Context } from "elysia";
19
+ import type { ResolvedRoute } from "../router.ts";
20
+ import { IS_DEV } from "../runtime-env.ts";
21
+ import type { LoaderContext } from "./assemble.ts";
22
+ import { generateIndexHtml } from "./shell.ts";
23
+
24
+ interface RenderResult {
25
+ headers: Record<string, string>;
26
+ html: string;
27
+ }
28
+
29
+ // ── Helpers ──────────────────────────────────────────────────────────────────
30
+
31
+ function catchRedirect(err: unknown): Response {
32
+ if (err instanceof Response) {
33
+ return err;
34
+ }
35
+ throw err;
36
+ }
37
+
38
+ async function renderForPath(
39
+ route: ResolvedRoute,
40
+ params: Record<string, string>,
41
+ root: RootLayout,
42
+ origin: string
43
+ ): Promise<RenderResult> {
44
+ const resolvedPath = resolvePath(route.pattern, params);
45
+ return await renderToHTML(
46
+ route,
47
+ {
48
+ params,
49
+ query: {},
50
+ request: new Request(`${origin}${resolvedPath}`),
51
+ headers: {},
52
+ cookie: {},
53
+ redirect: (url, status = 302) => new Response(null, { status, headers: { Location: url } }),
54
+ set: { headers: {} },
55
+ path: resolvedPath,
56
+ } as Context,
57
+ root
58
+ );
59
+ }
60
+
61
+ // ── Core pipeline ────────────────────────────────────────────────────────────
62
+
63
+ export async function renderToHTML(
64
+ route: ResolvedRoute,
65
+ ctx: Context,
66
+ root: RootLayout
67
+ ): Promise<RenderResult> {
68
+ const loaderResult = await runLoaders(route, ctx, root.route);
69
+
70
+ if (loaderResult.type === "redirect") {
71
+ throw loaderResult.response;
72
+ }
73
+
74
+ const { data, headers } = loaderResult;
75
+
76
+ const componentProps = {
77
+ ...data,
78
+ params: ctx.params,
79
+ query: ctx.query,
80
+ path: ctx.path,
81
+ };
82
+
83
+ const headData = buildHeadInjection(route.page?.head?.(componentProps));
84
+
85
+ const element = buildElement(route, componentProps, root.route);
86
+ const stream = await renderToReadableStream(element);
87
+ await stream.allReady;
88
+ const reactHtml = await streamToString(stream);
89
+
90
+ // Dev: self-fetch /_bun_hmr_entry (once, then cached) to get the Bun-processed
91
+ // HTML template with content-hashed chunk paths and HMR WebSocket client.
92
+ // Prod: read .furin/client/index.html from disk.
93
+ const template = IS_DEV
94
+ ? await getDevTemplate(new URL(ctx.request.url).origin)
95
+ : (getProductionTemplate() ?? generateIndexHtml());
96
+
97
+ return {
98
+ html: assembleHTML(template, headData, reactHtml, data),
99
+ headers,
100
+ };
101
+ }
102
+
103
+ // ── Public render functions ──────────────────────────────────────────────────
104
+
105
+ export async function renderToStream(
106
+ route: ResolvedRoute,
107
+ ctx: Context,
108
+ root: RootLayout
109
+ ): Promise<ReadableStream | Response> {
110
+ const response = await renderSSR(route, ctx, root);
111
+ if (!response.ok) {
112
+ return response;
113
+ }
114
+ return response.body ?? new ReadableStream();
115
+ }
116
+
117
+ export async function prerenderSSG(
118
+ route: ResolvedRoute,
119
+ params: Record<string, string>,
120
+ root: RootLayout,
121
+ origin = "http://localhost:3000"
122
+ ): Promise<string> {
123
+ const resolvedPath = resolvePath(route.pattern, params);
124
+
125
+ const cached = ssgCache.get(resolvedPath);
126
+ if (cached && !IS_DEV) {
127
+ return cached;
128
+ }
129
+
130
+ const result = await renderForPath(route, params, root, origin);
131
+
132
+ if (!IS_DEV) {
133
+ ssgCache.set(resolvedPath, result.html);
134
+ }
135
+
136
+ return result.html;
137
+ }
138
+
139
+ export async function renderSSR(
140
+ route: ResolvedRoute,
141
+ ctx: Context,
142
+ root: RootLayout
143
+ ): Promise<Response> {
144
+ try {
145
+ const loaderResult = await runLoaders(route, ctx, root.route);
146
+ if (loaderResult.type === "redirect") {
147
+ return loaderResult.response;
148
+ }
149
+
150
+ const { data, headers } = loaderResult;
151
+ const componentProps = { ...data, params: ctx.params, query: ctx.query, path: ctx.path };
152
+
153
+ const headData = buildHeadInjection(route.page.head?.(componentProps));
154
+ const template = IS_DEV
155
+ ? await getDevTemplate(new URL(ctx.request.url).origin)
156
+ : (getProductionTemplate() ?? generateIndexHtml());
157
+
158
+ // Phase 2: split template around placeholders
159
+ const { headPre, bodyPre, bodyPost } = splitTemplate(template);
160
+ const dataScript = `<script id="__FURIN_DATA__" type="application/json">${safeJson(data)}</script>`;
161
+
162
+ // Phase 3: start React render without awaiting allReady
163
+ const element = buildElement(route, componentProps, root.route);
164
+ const reactStream = await renderToReadableStream(element);
165
+
166
+ // Phase 4: pipe head + React stream + tail into a single ReadableStream
167
+ const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
168
+ const writer = writable.getWriter();
169
+ const enc = new TextEncoder();
170
+
171
+ (async () => {
172
+ await writer.write(enc.encode(headPre + headData + bodyPre));
173
+ const reader = reactStream.getReader();
174
+ for (;;) {
175
+ const { done, value } = await reader.read();
176
+ if (done) {
177
+ break;
178
+ }
179
+ await writer.write(value);
180
+ }
181
+ await writer.write(enc.encode(dataScript + bodyPost));
182
+ await writer.close();
183
+ })().catch((err) => writer.abort(err));
184
+
185
+ return new Response(readable, {
186
+ headers: {
187
+ "Content-Type": "text/html; charset=utf-8",
188
+ "Cache-Control": "no-cache, no-store, must-revalidate",
189
+ ...headers,
190
+ },
191
+ });
192
+ } catch (err) {
193
+ return catchRedirect(err);
194
+ }
195
+ }
196
+
197
+ export async function handleISR(route: ResolvedRoute, ctx: Context, root: RootLayout) {
198
+ const revalidate = route.page._route.revalidate ?? 60;
199
+ const params = ctx.params ?? {};
200
+ const cacheKey = resolvePath(route.pattern, params);
201
+
202
+ const cached = isrCache.get(cacheKey);
203
+
204
+ if (cached && !IS_DEV) {
205
+ const age = Date.now() - cached.generatedAt;
206
+ const isFresh = age < revalidate * 1000;
207
+
208
+ if (!isFresh) {
209
+ revalidateInBackground(route, params, cacheKey, revalidate, root, ctx);
210
+ }
211
+
212
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
213
+ ctx.set.headers["cache-control"] = isFresh
214
+ ? `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`
215
+ : "public, s-maxage=0, must-revalidate";
216
+
217
+ return cached.html;
218
+ }
219
+
220
+ try {
221
+ const result = await renderToHTML(route, ctx, root);
222
+
223
+ if (!IS_DEV) {
224
+ isrCache.set(cacheKey, { html: result.html, generatedAt: Date.now(), revalidate });
225
+ }
226
+
227
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
228
+ ctx.set.headers["cache-control"] =
229
+ `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`;
230
+ return result.html;
231
+ } catch (err) {
232
+ return catchRedirect(err);
233
+ }
234
+ }
235
+
236
+ // ── SSG warm-up ──────────────────────────────────────────────────────────────
237
+
238
+ /** Maximum number of concurrent `prerenderSSG` calls during SSG warm-up. */
239
+ const SSG_WARM_CONCURRENCY = 4;
240
+
241
+ /**
242
+ * Pre-renders all SSG routes that declare `staticParams` and populates the
243
+ * in-memory cache before the first real request arrives.
244
+ * Should be called from the Elysia `onStart` hook (production only).
245
+ *
246
+ * Uses a bounded worker pool (SSG_WARM_CONCURRENCY slots) so large sites
247
+ * with many routes × param sets cannot exhaust memory or CPU during startup.
248
+ * Failures are isolated per (route, params) pair and logged without aborting
249
+ * the rest of the warm-up.
250
+ */
251
+ export async function warmSSGCache(
252
+ routes: ResolvedRoute[],
253
+ root: RootLayout,
254
+ origin: string
255
+ ): Promise<void> {
256
+ const targets = routes.filter((r) => r.mode === "ssg" && r.page.staticParams);
257
+
258
+ // Collect all (route, params) render tasks, handling per-route errors early.
259
+ const tasks: Array<() => Promise<void>> = [];
260
+ for (const route of targets) {
261
+ let paramSets: Record<string, string>[];
262
+ try {
263
+ paramSets = (await route.page.staticParams?.()) ?? [];
264
+ } catch (err) {
265
+ console.error(`[furin] SSG warm-up failed for ${route.pattern}:`, err);
266
+ continue;
267
+ }
268
+ for (const params of paramSets) {
269
+ tasks.push(async () => {
270
+ try {
271
+ await prerenderSSG(route, params, root, origin);
272
+ } catch (err) {
273
+ console.error(`[furin] SSG prerender failed for ${route.pattern}:`, err);
274
+ }
275
+ });
276
+ }
277
+ }
278
+
279
+ if (tasks.length === 0) {
280
+ return;
281
+ }
282
+
283
+ // Drain the task queue with a fixed-size worker pool.
284
+ const queue = [...tasks];
285
+ const workers = Array.from({ length: Math.min(SSG_WARM_CONCURRENCY, tasks.length) }, async () => {
286
+ while (queue.length > 0) {
287
+ await queue.shift()?.();
288
+ }
289
+ });
290
+ await Promise.all(workers);
291
+ }
292
+
293
+ function revalidateInBackground(
294
+ route: ResolvedRoute,
295
+ params: Record<string, string>,
296
+ cacheKey: string,
297
+ revalidate: number,
298
+ root: RootLayout,
299
+ originalCtx: LoaderContext
300
+ ) {
301
+ renderForPath(route, params, root, new URL(originalCtx.request.url).origin)
302
+ .then((result) => {
303
+ isrCache.set(cacheKey, {
304
+ html: result.html,
305
+ generatedAt: Date.now(),
306
+ revalidate,
307
+ });
308
+ })
309
+ .catch((err: unknown) => {
310
+ console.error("[furin] ISR background revalidation failed:", err);
311
+ });
312
+ }
@@ -0,0 +1,67 @@
1
+ import type { Context } from "elysia";
2
+ import type { LoaderDeps, RuntimeRoute } from "../client";
3
+ import type { ResolvedRoute } from "../router";
4
+
5
+ export type LoaderResult =
6
+ | { type: "data"; data: Record<string, unknown>; headers: Record<string, string> }
7
+ | { type: "redirect"; response: Response };
8
+
9
+ export async function runLoaders(
10
+ route: ResolvedRoute,
11
+ ctx: Context,
12
+ rootLayout: RuntimeRoute
13
+ ): Promise<LoaderResult> {
14
+ try {
15
+ // Step 1: root loader runs first — provides global context (user, auth, etc.)
16
+ // Root data is merged into ctxWithRoot so all subsequent loaders receive it
17
+ // automatically (backward-compatible with the previous waterfall behaviour).
18
+ //
19
+ // deps is defined here so it can be passed to the root loader too.
20
+ // The root has no ancestors, so deps always returns an empty resolved promise.
21
+ const loaderMap = new Map<RuntimeRoute, Promise<Record<string, unknown>>>();
22
+
23
+ const deps: LoaderDeps = (routeRef) =>
24
+ loaderMap.get(routeRef as RuntimeRoute) ?? Promise.resolve({});
25
+
26
+ const rootData: Record<string, unknown> = rootLayout.loader
27
+ ? ((await rootLayout.loader({ ...ctx }, deps)) ?? {})
28
+ : {};
29
+ const ctxWithRoot = { ...ctx, ...rootData };
30
+
31
+ // Step 2: launch all ancestor + page loaders immediately in parallel.
32
+ // Each is stored in loaderMap keyed by object identity so that deps()
33
+ // can resolve a Promise for any route in the chain.
34
+ //
35
+ // Loaders are inserted in chain order (index 1, 2, 3 …) so when loader N
36
+ // calls `await deps(routeAtIndexM)` where M < N, the Promise is already
37
+ // present in the map — no deadlock is possible for backward dependencies.
38
+
39
+ for (let i = 1; i < route.routeChain.length; i++) {
40
+ const ancestor = route.routeChain[i];
41
+ if (ancestor?.loader) {
42
+ loaderMap.set(
43
+ ancestor,
44
+ Promise.resolve(ancestor.loader(ctxWithRoot, deps)).then((r) => r ?? {})
45
+ );
46
+ }
47
+ }
48
+
49
+ // Page loader — all ancestors are already in the map at this point.
50
+ const pagePromise: Promise<Record<string, unknown>> = route.page?.loader
51
+ ? Promise.resolve(route.page.loader(ctxWithRoot, deps)).then((r) => r ?? {})
52
+ : Promise.resolve({});
53
+
54
+ // Step 3: await all in parallel, then flat-merge results.
55
+ const results = await Promise.all([...loaderMap.values(), pagePromise]);
56
+ const data = Object.assign({}, rootData, ...results);
57
+ const headers: Record<string, string> = {};
58
+ Object.assign(headers, ctx.set.headers);
59
+
60
+ return { type: "data", data, headers };
61
+ } catch (err) {
62
+ if (err instanceof Response) {
63
+ return { type: "redirect", response: err };
64
+ }
65
+ throw err;
66
+ }
67
+ }
@@ -0,0 +1,128 @@
1
+ import type { HeadOptions, MetaDescriptor } from "../client.ts";
2
+
3
+ export function extractTitle(meta?: MetaDescriptor[]): string | undefined {
4
+ if (!meta) {
5
+ return undefined;
6
+ }
7
+ for (const entry of meta) {
8
+ if ("title" in entry) {
9
+ return (entry as { title: string }).title;
10
+ }
11
+ }
12
+ return undefined;
13
+ }
14
+
15
+ export function isMetaTag(entry: MetaDescriptor): boolean {
16
+ return !(
17
+ "title" in entry ||
18
+ "charSet" in entry ||
19
+ "script:ld+json" in entry ||
20
+ "tagName" in entry
21
+ );
22
+ }
23
+
24
+ export function escapeHtml(str: string): string {
25
+ return str
26
+ .replace(/&/g, "&amp;")
27
+ .replace(/</g, "&lt;")
28
+ .replace(/>/g, "&gt;")
29
+ .replace(/"/g, "&quot;")
30
+ .replace(/'/g, "&#039;");
31
+ }
32
+
33
+ export function safeJson(value: unknown): string {
34
+ return JSON.stringify(value).replace(/</g, "\\u003c");
35
+ }
36
+
37
+ export function renderAttrs(obj: Record<string, string | undefined>): string {
38
+ return Object.entries(obj)
39
+ .filter(([, v]) => v !== undefined)
40
+ .map(([k, v]) => `${k}="${escapeHtml(String(v))}"`)
41
+ .join(" ");
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Head injection helpers
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export function buildMetaParts(meta: MetaDescriptor[]): string[] {
49
+ const parts: string[] = [];
50
+ const title = extractTitle(meta);
51
+ if (title) {
52
+ parts.push(`<title>${escapeHtml(title)}</title>`);
53
+ }
54
+ for (const m of meta) {
55
+ if (isMetaTag(m)) {
56
+ parts.push(`<meta ${renderAttrs(m as Record<string, string>)} />`);
57
+ }
58
+ if ("script:ld+json" in m) {
59
+ parts.push(`<script type="application/ld+json">${safeJson(m["script:ld+json"])}</script>`);
60
+ }
61
+ }
62
+ return parts;
63
+ }
64
+
65
+ export function buildLinkParts(links: NonNullable<HeadOptions["links"]>): string[] {
66
+ return links.map((link) => `<link ${renderAttrs(link)} />`);
67
+ }
68
+
69
+ export function buildScriptParts(scripts: NonNullable<HeadOptions["scripts"]>): string[] {
70
+ return scripts.map((script) => {
71
+ const { children, ...rest } = script;
72
+ const attrs = renderAttrs(rest as Record<string, string | undefined>);
73
+ if (children) {
74
+ return `<script ${attrs}>${children}</script>`;
75
+ }
76
+ return `<script ${attrs}></script>`;
77
+ });
78
+ }
79
+
80
+ export function buildStyleParts(styles: NonNullable<HeadOptions["styles"]>): string[] {
81
+ return styles.map((style) => {
82
+ const typeAttr = style.type ? ` type="${escapeHtml(style.type)}"` : "";
83
+ return `<style${typeAttr}>${style.children}</style>`;
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Builds the string to inject into the <!--ssr-head--> placeholder.
89
+ * Handles title, meta tags, links, inline scripts, and inline styles from
90
+ * the page's `head()` function. CSS is handled by Bun (imported in user files).
91
+ */
92
+ export function buildHeadInjection(headData: HeadOptions | undefined): string {
93
+ const parts: string[] = [];
94
+
95
+ if (headData?.meta) {
96
+ parts.push(...buildMetaParts(headData.meta));
97
+ }
98
+
99
+ if (headData?.links) {
100
+ parts.push(...buildLinkParts(headData.links));
101
+ }
102
+
103
+ if (headData?.scripts) {
104
+ parts.push(...buildScriptParts(headData.scripts));
105
+ }
106
+
107
+ if (headData?.styles) {
108
+ parts.push(...buildStyleParts(headData.styles));
109
+ }
110
+
111
+ return parts.length > 0 ? `\n ${parts.join("\n ")}\n` : "";
112
+ }
113
+
114
+ export function generateIndexHtml(): string {
115
+ return `<!DOCTYPE html>
116
+ <html lang="en">
117
+ <head>
118
+ <meta charset="UTF-8">
119
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
120
+ <!--ssr-head-->
121
+ </head>
122
+ <body>
123
+ <div id="root"><!--ssr-outlet--></div>
124
+ <script type="module" src="./_hydrate.tsx"></script>
125
+ </body>
126
+ </html>
127
+ `;
128
+ }
@@ -0,0 +1,54 @@
1
+ // ── Dev template ─────────────────────────────────────────────────────────────
2
+
3
+ import { readFileSync } from "node:fs";
4
+
5
+ const devTemplatePromises = new Map<string, Promise<string>>();
6
+ let _prodTemplatePath: string | null = null;
7
+ let _prodTemplateContent: string | null = null;
8
+
9
+ export function getDevTemplate(origin: string): Promise<string> {
10
+ const cached = devTemplatePromises.get(origin);
11
+ if (cached) {
12
+ return cached;
13
+ }
14
+
15
+ const promise = fetch(`${origin}/_bun_hmr_entry`)
16
+ .then((r) => {
17
+ if (!r.ok) {
18
+ throw new Error(`/_bun_hmr_entry returned ${r.status}`);
19
+ }
20
+ return r.text();
21
+ })
22
+ .catch((err) => {
23
+ devTemplatePromises.delete(origin);
24
+ throw err;
25
+ });
26
+
27
+ devTemplatePromises.set(origin, promise);
28
+ return promise;
29
+ }
30
+
31
+ export function setProductionTemplatePath(path: string | null): void {
32
+ _prodTemplatePath = path;
33
+ _prodTemplateContent = null;
34
+ }
35
+
36
+ export function setProductionTemplateContent(content: string): void {
37
+ _prodTemplatePath = null;
38
+ _prodTemplateContent = content;
39
+ }
40
+
41
+ export function getProductionTemplate(): string | null {
42
+ if (_prodTemplateContent !== null) {
43
+ return _prodTemplateContent;
44
+ }
45
+ if (!_prodTemplatePath) {
46
+ return null;
47
+ }
48
+ try {
49
+ _prodTemplateContent = readFileSync(_prodTemplatePath, "utf8");
50
+ return _prodTemplateContent;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }