@janit/fu 0.0.2

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/dist/dev.js ADDED
@@ -0,0 +1,167 @@
1
+ // Dev driver: rolldown DevEngine (HMR patches) + crossws (transport) +
2
+ // nitro dev server (SSR). No Vite anywhere.
3
+ import { DevEngine } from "rolldown/experimental";
4
+ import { serve } from "crossws/server";
5
+ import { build as nitroBuild, createDevServer, createNitro, prepare } from "nitro/builder";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { bootModule, css, jsx, optional, ssrModule, virtual, walk } from "./plugins.js";
9
+ /**
10
+ * Directory holding the framework's own runtime modules, whose paths are handed
11
+ * to rolldown. `import.meta.dirname` is undefined when this module is loaded
12
+ * from a remote URL, and a bundler cannot fetch `https:` modules anyway — so
13
+ * fail with the reason rather than a TypeError three frames later.
14
+ */
15
+ const HERE = import.meta.dirname ?? remoteFrameworkError();
16
+ /**
17
+ * Extension of the framework's own runtime modules: `.ts` when running from
18
+ * source (this repo, or JSR), `.js` when running from the compiled npm build.
19
+ */
20
+ const EXT = import.meta.url.endsWith(".js") ? ".js" : ".ts";
21
+ function remoteFrameworkError() {
22
+ throw new Error("fu: the framework is loaded from a remote URL (" + import.meta.url + "), " +
23
+ "so its runtime modules cannot be handed to the bundler. Install it " +
24
+ "instead — `npm:@janit/fu` in a Deno import map, or `npm i @janit/fu` — " +
25
+ "so it resolves to a real directory.");
26
+ }
27
+ export async function dev(opts) {
28
+ const root = path.resolve(opts.root);
29
+ const port = opts.port ?? 1337;
30
+ // Bind all interfaces so the dev server is reachable from other machines
31
+ // and from inside containers, not just loopback.
32
+ const hostname = opts.hostname ?? "0.0.0.0";
33
+ const hmrPort = port + 1;
34
+ const clientDir = path.join(root, "dist/client");
35
+ const genDir = path.join(root, ".fu");
36
+ const routeFiles = walk(root, path.join(root, "routes"));
37
+ const islandFiles = walk(root, path.join(root, "islands"));
38
+ fs.rmSync(clientDir, { recursive: true, force: true });
39
+ fs.mkdirSync(clientDir, { recursive: true });
40
+ const sheets = new Map();
41
+ const peers = new Map();
42
+ let cssVersion = 0;
43
+ const writeSheets = () => {
44
+ if (sheets.size)
45
+ fs.writeFileSync(path.join(clientDir, "style.css"), [...sheets.values()].join("\n"));
46
+ };
47
+ // `implement` takes the runtime SOURCE, not a path — a path gets inlined
48
+ // literally and parsed as a regex. `$ADDR` is only substituted in rolldown's
49
+ // own default runtime, so do it here.
50
+ const hmrRuntime = fs.readFileSync(path.join(HERE, "hmr-runtime.js"), "utf8")
51
+ .replaceAll("$ADDR", `localhost:${hmrPort}`);
52
+ const engine = await DevEngine.create({
53
+ input: { boot: "fu:boot" },
54
+ plugins: [
55
+ virtual({ "fu:boot": bootModule(path.join(HERE, `client${EXT}`), islandFiles, root) }),
56
+ css(sheets),
57
+ jsx({ hmr: true }),
58
+ ],
59
+ platform: "browser",
60
+ moduleTypes: { ".css": "js" },
61
+ experimental: { devMode: { host: "localhost", port: hmrPort, implement: hmrRuntime } },
62
+ }, { dir: clientDir, format: "esm", entryFileNames: "[name].js", chunkFileNames: "[name].js" }, {
63
+ watch: { enabled: true },
64
+ onOutput(o) {
65
+ if (o instanceof Error)
66
+ return console.error("[fu] client build failed:", o.message);
67
+ writeSheets();
68
+ },
69
+ onHmrUpdates(r) {
70
+ if (r instanceof Error)
71
+ return console.error("[fu] hmr error:", r.message);
72
+ writeSheets();
73
+ for (const { clientId, update } of r.updates) {
74
+ const peer = peers.get(clientId);
75
+ if (!peer || update.type === "Noop")
76
+ continue;
77
+ if (update.type === "FullReload") {
78
+ peer.send(JSON.stringify({ type: "hmr:reload" }));
79
+ continue;
80
+ }
81
+ // Always deliver the patch for real. Reporting a payload as delivered
82
+ // when it was not corrupts per-client shipped-state and updates stop
83
+ // firing silently.
84
+ fs.writeFileSync(path.join(clientDir, update.filename), update.code);
85
+ peer.send(JSON.stringify({
86
+ type: "hmr:update",
87
+ path: "/" + update.filename,
88
+ url: "/" + update.filename,
89
+ changedIds: update.changedIds,
90
+ }));
91
+ engine.notifyPayloadDelivered(update.filename);
92
+ if ((update.changedIds ?? []).some((id) => id.endsWith(".css"))) {
93
+ peer.send(JSON.stringify({ type: "fu:css", href: `/style.css?v=${++cssVersion}` }));
94
+ }
95
+ }
96
+ },
97
+ });
98
+ await engine.run();
99
+ await engine.ensureLatestBuildOutput();
100
+ // Transport. Inline hooks behave uniformly across node/deno/bun; returning a
101
+ // plain `{crossws}` object from `fetch` fails on Deno.
102
+ const clientIdOf = (peer) => {
103
+ const raw = peer?.request?.url;
104
+ if (!raw)
105
+ return null;
106
+ try {
107
+ return new URL(raw, "http://localhost").searchParams.get("clientId");
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ };
113
+ serve({
114
+ port: hmrPort,
115
+ fetch: () => new Response("fu hmr"),
116
+ websocket: {
117
+ async open(peer) {
118
+ const id = clientIdOf(peer);
119
+ if (!id)
120
+ return;
121
+ peers.set(id, peer);
122
+ await engine.registerClient(id);
123
+ peer.send(JSON.stringify({ type: "connected" }));
124
+ },
125
+ async close(peer) {
126
+ const id = clientIdOf(peer);
127
+ if (!id)
128
+ return;
129
+ peers.delete(id);
130
+ await engine.removeClient(id);
131
+ },
132
+ },
133
+ });
134
+ // SSR.
135
+ fs.rmSync(genDir, { recursive: true, force: true });
136
+ fs.mkdirSync(genDir, { recursive: true });
137
+ const ssrEntry = path.join(genDir, "ssr.ts");
138
+ const assets = `{ js: [{ href: "/boot.js" }], css: [{ href: "/style.css" }] }`;
139
+ fs.writeFileSync(ssrEntry, ssrModule({
140
+ renderPath: path.join(HERE, `render${EXT}`),
141
+ routeFiles,
142
+ root,
143
+ assets,
144
+ appPath: optional(root, "app.ts", "app.tsx"),
145
+ shellPath: optional(root, "routes/_app.tsx"),
146
+ errorPath: optional(root, "routes/_error.tsx"),
147
+ }));
148
+ const nitro = await createNitro({
149
+ dev: true,
150
+ rootDir: root,
151
+ serverDir: genDir,
152
+ scanDirs: [],
153
+ publicAssets: [{ dir: clientDir, baseURL: "/" }],
154
+ handlers: [{ route: "/**", handler: ssrEntry, format: "web", lazy: false }],
155
+ rollupConfig: {
156
+ plugins: [css(new Map()), jsx({ stampIslands: true })],
157
+ moduleTypes: { ".css": "js" },
158
+ },
159
+ });
160
+ const server = createDevServer(nitro);
161
+ server.listen({ port, hostname });
162
+ // Order matters: listen -> prepare -> build. `build` starts the dev runner.
163
+ await prepare(nitro);
164
+ await nitroBuild(nitro);
165
+ console.log(`[fu] dev http://localhost:${port} (bound ${hostname}:${port})`);
166
+ }
167
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { RouteError } from "./types.ts";
2
+ /**
3
+ * Throw from a handler or middleware to produce a specific status.
4
+ *
5
+ * ```ts
6
+ * if (!session) throw new HttpError(403, "Not your todo");
7
+ * ```
8
+ */
9
+ export declare class HttpError extends Error {
10
+ readonly status: number;
11
+ constructor(status: number, message?: string);
12
+ }
13
+ export declare function statusText(status: number): string;
14
+ /**
15
+ * Normalise anything thrown into a status and a message.
16
+ *
17
+ * A `status` property is honoured wherever it appears, so errors from other
18
+ * libraries (h3, fetch wrappers) map sensibly instead of collapsing to 500.
19
+ */
20
+ export declare function toRouteError(err: unknown): RouteError;
package/dist/errors.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Throw from a handler or middleware to produce a specific status.
3
+ *
4
+ * ```ts
5
+ * if (!session) throw new HttpError(403, "Not your todo");
6
+ * ```
7
+ */
8
+ export class HttpError extends Error {
9
+ status;
10
+ constructor(status, message) {
11
+ super(message ?? statusText(status));
12
+ this.name = "HttpError";
13
+ this.status = status;
14
+ }
15
+ }
16
+ const TEXT = {
17
+ 400: "Bad Request",
18
+ 401: "Unauthorized",
19
+ 403: "Forbidden",
20
+ 404: "Not Found",
21
+ 405: "Method Not Allowed",
22
+ 409: "Conflict",
23
+ 410: "Gone",
24
+ 422: "Unprocessable Content",
25
+ 429: "Too Many Requests",
26
+ 500: "Internal Server Error",
27
+ 502: "Bad Gateway",
28
+ 503: "Service Unavailable",
29
+ };
30
+ export function statusText(status) {
31
+ return TEXT[status] ?? (status >= 500 ? "Internal Server Error" : "Error");
32
+ }
33
+ /**
34
+ * Normalise anything thrown into a status and a message.
35
+ *
36
+ * A `status` property is honoured wherever it appears, so errors from other
37
+ * libraries (h3, fetch wrappers) map sensibly instead of collapsing to 500.
38
+ */
39
+ export function toRouteError(err) {
40
+ if (err instanceof HttpError) {
41
+ return { status: err.status, message: err.message, cause: err };
42
+ }
43
+ const status = err?.status;
44
+ if (typeof status === "number" && status >= 400 && status <= 599) {
45
+ const message = err.message;
46
+ return {
47
+ status,
48
+ message: typeof message === "string" && message ? message : statusText(status),
49
+ cause: err,
50
+ };
51
+ }
52
+ return { status: 500, message: statusText(500), cause: err };
53
+ }
54
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,OAAgB;QAC1C,KAAK,CAAC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,IAAI,GAA2B;IACnC,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,oBAAoB;IACzB,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,qBAAqB;CAC3B,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,IAAI,GAAG,YAAY,SAAS,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IAClE,CAAC;IACD,MAAM,MAAM,GAAI,GAAmC,EAAE,MAAM,CAAC;IAC5D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QACjE,MAAM,OAAO,GAAI,GAA6B,CAAC,OAAO,CAAC;QACvD,OAAO;YACL,MAAM;YACN,OAAO,EAAE,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;YAC9E,KAAK,EAAE,GAAG;SACX,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,112 @@
1
+ // @ts-check
2
+ // Fresh Urquell HMR client runtime. Rolldown's default runtime registers new module
3
+ // factories but never *applies* them; this one does the apply walk:
4
+ // swap the module, re-run its factory, then fire its accept callbacks.
5
+
6
+ /** @type {any} */
7
+ var BaseDevRuntime = DevRuntime;
8
+
9
+ class ModuleHotContext {
10
+ /** @type {{ deps: string[], fn: (mod: any) => void }[]} */
11
+ acceptCallbacks = [];
12
+ constructor(moduleId, devRuntime) {
13
+ this.moduleId = moduleId;
14
+ this.devRuntime = devRuntime;
15
+ }
16
+ accept(...args) {
17
+ if (args.length === 1) {
18
+ this.acceptCallbacks.push({ deps: [this.moduleId], fn: args[0] });
19
+ } else if (args.length !== 0) {
20
+ throw new Error('Invalid arguments for `import.meta.hot.accept`');
21
+ }
22
+ }
23
+ invalidate() {
24
+ socket.send(JSON.stringify({ type: 'hmr:invalidate', moduleId: this.moduleId }));
25
+ }
26
+ }
27
+
28
+ class FuDevRuntime extends BaseDevRuntime {
29
+ /** @type {Map<string, ModuleHotContext>} */
30
+ moduleHotContexts = new Map();
31
+ createModuleHotContext(moduleId) {
32
+ const ctx = new ModuleHotContext(moduleId, this);
33
+ this.moduleHotContexts.set(moduleId, ctx);
34
+ return ctx;
35
+ }
36
+ }
37
+
38
+ const clientId = crypto.randomUUID();
39
+ const addr = new URL('ws://$ADDR');
40
+ addr.searchParams.set('clientId', clientId);
41
+ const socket = new WebSocket(addr);
42
+
43
+ /** @type {any} */
44
+ const runtime = new FuDevRuntime(clientId);
45
+ globalThis.__rolldown_runtime__ ??= runtime;
46
+
47
+ /**
48
+ * Apply one patch: import it (registering new factories), then for every
49
+ * changed module that accepted itself, drop its cache, re-run the factory and
50
+ * hand the fresh exports to its accept callbacks. Anything that did not accept
51
+ * falls back to a full reload.
52
+ */
53
+ async function applyPatch(url, allChangedIds) {
54
+ const rt = globalThis.__rolldown_runtime__;
55
+ // Stylesheets are swapped via the <link>, so they never need to accept and
56
+ // must not drag the page into a full reload.
57
+ const changedIds = (allChangedIds || []).filter((id) => !id.endsWith('.css'));
58
+ const selfAccepting = changedIds.filter((id) => {
59
+ const ctx = rt.moduleHotContexts.get(id);
60
+ return ctx && ctx.acceptCallbacks.length > 0;
61
+ });
62
+ try {
63
+ await import(url + (url.includes('?') ? '&' : '?') + 't=' + Date.now());
64
+ } catch (err) {
65
+ console.error('[hmr] failed to load patch', url, err);
66
+ location.reload();
67
+ return;
68
+ }
69
+ if (!changedIds || changedIds.length === 0) return;
70
+ if (selfAccepting.length !== changedIds.length) {
71
+ console.debug('[hmr] some modules did not accept; reloading');
72
+ location.reload();
73
+ return;
74
+ }
75
+ for (const id of selfAccepting) {
76
+ rt.removeModuleCache(id);
77
+ rt.initModule(id);
78
+ const exports = rt.loadExports(id);
79
+ const ctx = rt.moduleHotContexts.get(id);
80
+ for (const { fn } of ctx ? ctx.acceptCallbacks : []) fn(exports);
81
+ }
82
+ console.debug('[hmr] applied', selfAccepting.join(', '));
83
+ }
84
+
85
+ /**
86
+ * Swap the stylesheet in place. A new <link> is inserted and the old one is
87
+ * only removed once the replacement has loaded, so the page never flashes
88
+ * unstyled.
89
+ */
90
+ function swapStylesheet(href) {
91
+ const links = [...document.querySelectorAll('link[rel="stylesheet"]')];
92
+ const old = links[links.length - 1];
93
+ const next = document.createElement('link');
94
+ next.rel = 'stylesheet';
95
+ next.href = href;
96
+ next.onload = () => { if (old && old !== next) old.remove(); };
97
+ (old ? old.parentNode : document.head).insertBefore(next, old ? old.nextSibling : null);
98
+ console.debug('[hmr] css swapped ->', href);
99
+ }
100
+
101
+ socket.onmessage = function (event) {
102
+ const data = JSON.parse(event.data);
103
+ if (data.type === 'connected') {
104
+ console.debug('[hmr] connected');
105
+ } else if (data.type === 'hmr:update') {
106
+ applyPatch(data.url, data.changedIds);
107
+ } else if (data.type === 'fu:css') {
108
+ swapStylesheet(data.href);
109
+ } else if (data.type === 'hmr:reload') {
110
+ location.reload();
111
+ }
112
+ };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Fresh Urquell — a minimal islands framework on Preact, rolldown and Nitro.
3
+ *
4
+ * This entrypoint is the bundler-agnostic core: it needs only a route manifest
5
+ * and an asset list, both plain data. Drivers live in `./build.ts` and
6
+ * `./dev.ts`.
7
+ */
8
+ export { App, compose } from "./app.ts";
9
+ export { HttpError, statusText, toRouteError } from "./errors.ts";
10
+ export { createHandler, document, renderError } from "./render.ts";
11
+ export type { HandlerParts, ShellProps } from "./render.ts";
12
+ export { buildRoutes, filePathToPattern, match } from "./router.ts";
13
+ export type { Matched, Route } from "./router.ts";
14
+ export type { Asset, Assets, Ctx, FuOptions, Head, Middleware, PageContext, RouteError, RouteHandler, RouteManifest, RouteModule, } from "./types.ts";
package/dist/mod.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Fresh Urquell — a minimal islands framework on Preact, rolldown and Nitro.
3
+ *
4
+ * This entrypoint is the bundler-agnostic core: it needs only a route manifest
5
+ * and an asset list, both plain data. Drivers live in `./build.ts` and
6
+ * `./dev.ts`.
7
+ */
8
+ export { App, compose } from "./app.js";
9
+ export { HttpError, statusText, toRouteError } from "./errors.js";
10
+ export { createHandler, document, renderError } from "./render.js";
11
+ export { buildRoutes, filePathToPattern, match } from "./router.js";
12
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { Plugin } from "rolldown";
2
+ export interface JsxOptions {
3
+ /** Stamp island exports with their module id (SSR build only). */
4
+ stampIslands?: boolean;
5
+ /** Inject `import.meta.hot.accept` into islands (dev client only). */
6
+ hmr?: boolean;
7
+ }
8
+ /**
9
+ * TypeScript + Preact JSX via oxc — the same native transform Vite delegates
10
+ * to. Also carries the two island-specific source rewrites, since both need the
11
+ * transformed output.
12
+ */
13
+ export declare function jsx(opts?: JsxOptions): Plugin;
14
+ /**
15
+ * Mark every exported component so the SSR renderer can spot a hydration
16
+ * boundary from the component function alone.
17
+ *
18
+ * Uses oxc's parser rather than a regex: `export const X = () => {}` is the
19
+ * most natural way to write a Preact component, and a regex over `export
20
+ * function` silently misses it — the island renders and then never hydrates,
21
+ * with no error anywhere.
22
+ *
23
+ * An anonymous `export default () => {}` has no binding to stamp, so it is
24
+ * rewritten into a named const. That is the one case that shifts positions and
25
+ * therefore invalidates the sourcemap, which the caller drops.
26
+ */
27
+ export declare function stampIslands(js: string, key: string, id: string): {
28
+ code: string;
29
+ rewrote: boolean;
30
+ };
31
+ /**
32
+ * CSS. Rolldown removed CSS bundling and ships no CSS builtin, so the framework
33
+ * owns the pipeline via lightningcss:
34
+ * `*.module.css` -> CSS Modules (scoped names + JS exports object)
35
+ * `*.css` -> global stylesheet
36
+ * Native nesting, `@layer` and `color-mix` pass straight through.
37
+ *
38
+ * Pair this with `moduleTypes: { ".css": "js" }`, or rolldown classifies the
39
+ * module by extension and refuses to bundle it.
40
+ */
41
+ export declare function css(collected: Map<string, string>): Plugin;
42
+ /** Serve generated modules by exact id. */
43
+ export declare function virtual(mods: Record<string, string>): Plugin;
44
+ export declare function hash(s: string): string;
45
+ /**
46
+ * Recursively list source files under `dir`, as root-relative "/a/b.tsx".
47
+ * Underscore-prefixed files are skipped: they are framework files
48
+ * (`routes/_app.tsx`), not routes.
49
+ */
50
+ export declare function walk(root: string, dir: string, out?: string[]): string[];
51
+ /** Absolute path to an optional project file, or null when absent. */
52
+ export declare function optional(root: string, ...names: string[]): string | null;
53
+ /** Generated module bodies both drivers need. */
54
+ export declare function bootModule(clientPath: string, islandFiles: string[], root: string): string;
55
+ export interface SsrModuleOptions {
56
+ renderPath: string;
57
+ routeFiles: string[];
58
+ root: string;
59
+ /** Serialized Assets literal. */
60
+ assets: string;
61
+ /** Absolute path to the project's `app.ts`, if it has one. */
62
+ appPath?: string | null;
63
+ /** Absolute path to the project's `routes/_app.tsx`, if it has one. */
64
+ shellPath?: string | null;
65
+ /** Absolute path to the project's `routes/_error.tsx`, if it has one. */
66
+ errorPath?: string | null;
67
+ }
68
+ export declare function ssrModule(o: SsrModuleOptions): string;