@pracht/vite-plugin 0.0.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,50 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/index.d.ts
4
+ declare const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
5
+ declare const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
6
+ /**
7
+ * An adapter object that bridges pracht's platform-agnostic core to a specific
8
+ * deployment target. Built-in adapters are provided by `@pracht/adapter-node`,
9
+ * `@pracht/adapter-cloudflare`, and `@pracht/adapter-vercel`. You can also
10
+ * supply a custom adapter that conforms to this interface.
11
+ */
12
+ interface PrachtAdapter {
13
+ /** A short identifier used at build time (e.g. "node", "cloudflare", "vercel"). */
14
+ id: string;
15
+ /**
16
+ * Extra import statements that must appear at the top of the generated
17
+ * `virtual:pracht/server` module. Return an empty string if none are needed.
18
+ */
19
+ serverImports: string;
20
+ /**
21
+ * Returns the JavaScript source code appended to the generated
22
+ * `virtual:pracht/server` module. This is where the adapter wires up its
23
+ * request handler or default export.
24
+ */
25
+ createServerEntryModule(): string;
26
+ }
27
+ type RenderMode = "spa" | "ssr" | "ssg" | "isg";
28
+ interface PrachtPluginOptions {
29
+ appFile?: string;
30
+ routesDir?: string;
31
+ shellsDir?: string;
32
+ middlewareDir?: string;
33
+ apiDir?: string;
34
+ serverDir?: string;
35
+ adapter?: PrachtAdapter;
36
+ /** Enable file-system pages routing by pointing to the pages directory (e.g. "/src/pages"). */
37
+ pagesDir?: string;
38
+ /** Default render mode for pages when RENDER_MODE is not exported. Defaults to "ssr". */
39
+ pagesDefaultRender?: RenderMode;
40
+ }
41
+ declare function pracht(options?: PrachtPluginOptions): Promise<Plugin[]>;
42
+ declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
43
+ root?: string;
44
+ }): string;
45
+ declare function createPrachtServerModuleSource(options?: PrachtPluginOptions, buildOptions?: {
46
+ root?: string;
47
+ }): string;
48
+ declare function createPrachtRegistryModuleSource(options?: PrachtPluginOptions): string;
49
+ //#endregion
50
+ export { PRACHT_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PrachtAdapter, PrachtPluginOptions, RenderMode, createPrachtClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, pracht };
package/dist/index.mjs ADDED
@@ -0,0 +1,382 @@
1
+ import { generatePagesManifestSource, scanPagesDirectory } from "./pages-router.mjs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import preact from "@preact/preset-vite";
5
+ //#region src/index.ts
6
+ const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
7
+ const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
8
+ const CLIENT_BROWSER_PATH = "/@pracht/client.js";
9
+ function isClientModule(id) {
10
+ return id === "virtual:pracht/client" || id === CLIENT_BROWSER_PATH || id.endsWith("virtual:pracht/client");
11
+ }
12
+ function isServerModule(id) {
13
+ return id === "virtual:pracht/server" || id.endsWith("virtual:pracht/server");
14
+ }
15
+ function createDefaultNodeAdapter() {
16
+ return {
17
+ id: "node",
18
+ serverImports: "import { resolveApp, resolveApiRoutes } from \"@pracht/core\";",
19
+ createServerEntryModule() {
20
+ return [
21
+ "import { existsSync, readFileSync } from \"node:fs\";",
22
+ "import { createServer } from \"node:http\";",
23
+ "import { dirname, resolve } from \"node:path\";",
24
+ "import { fileURLToPath, pathToFileURL } from \"node:url\";",
25
+ "import { createNodeRequestHandler } from \"@pracht/adapter-node\";",
26
+ "",
27
+ "const serverDir = dirname(fileURLToPath(import.meta.url));",
28
+ "const staticDir = resolve(serverDir, \"../client\");",
29
+ "const isgManifestPath = resolve(serverDir, \"isg-manifest.json\");",
30
+ "const isgManifest = existsSync(isgManifestPath)",
31
+ " ? JSON.parse(readFileSync(isgManifestPath, \"utf-8\"))",
32
+ " : {};",
33
+ "",
34
+ "export const handler = createNodeRequestHandler({",
35
+ " app: resolvedApp,",
36
+ " registry,",
37
+ " staticDir,",
38
+ " isgManifest,",
39
+ " apiRoutes,",
40
+ " clientEntryUrl: clientEntryUrl ?? undefined,",
41
+ " cssManifest,",
42
+ " jsManifest,",
43
+ "});",
44
+ "",
45
+ "const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;",
46
+ "if (entryHref && import.meta.url === entryHref) {",
47
+ " const server = createServer(handler);",
48
+ " const port = Number(process.env.PORT ?? 3000);",
49
+ " server.listen(port, () => {",
50
+ " console.log(`pracht node server listening on http://localhost:${port}`);",
51
+ " });",
52
+ "}",
53
+ ""
54
+ ].join("\n");
55
+ }
56
+ };
57
+ }
58
+ const DEFAULTS = {
59
+ appFile: "/src/routes.ts",
60
+ middlewareDir: "/src/middleware",
61
+ routesDir: "/src/routes",
62
+ shellsDir: "/src/shells",
63
+ apiDir: "/src/api",
64
+ serverDir: "/src/server",
65
+ adapter: createDefaultNodeAdapter(),
66
+ pagesDir: "",
67
+ pagesDefaultRender: "ssr"
68
+ };
69
+ async function pracht(options = {}) {
70
+ const resolved = resolveOptions(options);
71
+ const isPagesMode = !!resolved.pagesDir;
72
+ let root = process.cwd();
73
+ if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
74
+ const prachtPlugin = {
75
+ name: "pracht",
76
+ enforce: "pre",
77
+ config() {
78
+ return {
79
+ appType: "custom",
80
+ build: { rollupOptions: { output: { manualChunks(id) {
81
+ if (id.includes("node_modules/preact") || id.includes("node_modules/preact-suspense")) return "vendor";
82
+ } } } }
83
+ };
84
+ },
85
+ configResolved(config) {
86
+ root = config.root;
87
+ },
88
+ resolveId(id) {
89
+ if (isClientModule(id)) return PRACHT_CLIENT_MODULE_ID;
90
+ if (isServerModule(id)) return PRACHT_SERVER_MODULE_ID;
91
+ return null;
92
+ },
93
+ load(id) {
94
+ if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
95
+ if (isServerModule(id)) return createPrachtServerModuleSource(resolved, { root });
96
+ return null;
97
+ },
98
+ configureServer(server) {
99
+ if (isPagesMode) {
100
+ const abs = resolve(root, resolved.pagesDir.slice(1));
101
+ server.watcher.on("add", (f) => {
102
+ if (f.startsWith(abs)) server.restart();
103
+ });
104
+ server.watcher.on("unlink", (f) => {
105
+ if (f.startsWith(abs)) server.restart();
106
+ });
107
+ }
108
+ if (resolved.adapter.id === "cloudflare") return;
109
+ return () => {
110
+ server.middlewares.use(createDevSSRMiddleware(server, resolved));
111
+ };
112
+ },
113
+ handleHotUpdate({ file, server }) {
114
+ const root = server.config.root;
115
+ const relative = file.startsWith(root) ? file.slice(root.length) : file;
116
+ if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
117
+ const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
118
+ const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
119
+ if (clientMod) server.moduleGraph.invalidateModule(clientMod);
120
+ if (serverMod) server.moduleGraph.invalidateModule(serverMod);
121
+ return;
122
+ }
123
+ if (!isPagesMode && relative === resolved.appFile) {
124
+ server.restart();
125
+ return [];
126
+ }
127
+ if ([
128
+ resolved.routesDir,
129
+ resolved.shellsDir,
130
+ resolved.middlewareDir,
131
+ resolved.apiDir,
132
+ resolved.serverDir
133
+ ].some((dir) => relative.startsWith(dir))) {
134
+ const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
135
+ if (serverMod) server.moduleGraph.invalidateModule(serverMod);
136
+ }
137
+ }
138
+ };
139
+ const plugins = [...preact(), prachtPlugin];
140
+ if (resolved.adapter.id === "cloudflare") {
141
+ const { cloudflare } = await import("@cloudflare/vite-plugin");
142
+ plugins.push(...cloudflare({ config: { main: "virtual:pracht/server" } }));
143
+ }
144
+ return plugins;
145
+ }
146
+ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
147
+ const resolved = resolveOptions(options);
148
+ const isPagesMode = !!resolved.pagesDir;
149
+ const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
150
+ const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
151
+ const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
152
+ return [
153
+ "import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core\";",
154
+ appImport,
155
+ "",
156
+ `const routeModules = import.meta.glob(${JSON.stringify(routeGlob)});`,
157
+ `const shellModules = import.meta.glob(${JSON.stringify(shellGlob)});`,
158
+ "",
159
+ "const resolvedApp = resolveApp(app);",
160
+ "",
161
+ "function findModuleKey(modules, file) {",
162
+ " if (file in modules) return file;",
163
+ " const suffix = file.replace(/^\\.\\//,\"\");",
164
+ " for (const key of Object.keys(modules)) {",
165
+ " if (key.endsWith(\"/\" + suffix) || key.endsWith(suffix)) return key;",
166
+ " }",
167
+ " return null;",
168
+ "}",
169
+ "",
170
+ "const state = readHydrationState();",
171
+ "const root = document.getElementById(\"pracht-root\");",
172
+ "if (state && root) {",
173
+ " initClientRouter({",
174
+ " app: resolvedApp,",
175
+ " routeModules,",
176
+ " shellModules,",
177
+ " initialState: state,",
178
+ " root,",
179
+ " findModuleKey,",
180
+ " });",
181
+ "}",
182
+ ""
183
+ ].join("\n");
184
+ }
185
+ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
186
+ const resolved = resolveOptions(options);
187
+ const isPagesMode = !!resolved.pagesDir;
188
+ const registrySource = createPrachtRegistryModuleSource(resolved);
189
+ const clientBuild = readClientBuildAssets(buildOptions.root);
190
+ const adapter = resolved.adapter;
191
+ const source = [
192
+ adapter?.serverImports ? adapter.serverImports : "import { resolveApp, resolveApiRoutes } from \"@pracht/core\";",
193
+ isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
194
+ "",
195
+ registrySource,
196
+ "",
197
+ "export const resolvedApp = resolveApp(app);",
198
+ `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
199
+ `export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
200
+ `export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? CLIENT_BROWSER_PATH)};`,
201
+ `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
202
+ `export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
203
+ ""
204
+ ];
205
+ if (adapter) source.push(adapter.createServerEntryModule());
206
+ return source.join("\n");
207
+ }
208
+ function createPrachtRegistryModuleSource(options = {}) {
209
+ const resolved = resolveOptions(options);
210
+ const isPagesMode = !!resolved.pagesDir;
211
+ const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md}`;
212
+ const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md}`;
213
+ return [
214
+ `export const routeModules = import.meta.glob(${JSON.stringify(routeGlob)});`,
215
+ `export const shellModules = import.meta.glob(${JSON.stringify(shellGlob)});`,
216
+ `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx,md}`)});`,
217
+ `export const apiModules = import.meta.glob(${JSON.stringify(`${resolved.apiDir}/**/*.{ts,js}`)});`,
218
+ `export const dataModules = import.meta.glob(${JSON.stringify(`${resolved.serverDir}/**/*.{ts,js}`)});`,
219
+ "",
220
+ "export const registry = {",
221
+ " routeModules,",
222
+ " shellModules,",
223
+ " middlewareModules,",
224
+ " apiModules,",
225
+ " dataModules,",
226
+ "};"
227
+ ].join("\n");
228
+ }
229
+ function generatePagesAppInlineSource(options, root = process.cwd()) {
230
+ const absPagesDir = resolve(root, options.pagesDir.slice(1));
231
+ return generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
232
+ pagesDir: absPagesDir,
233
+ pagesDefaultRender: options.pagesDefaultRender,
234
+ pagesDirPrefix: options.pagesDir
235
+ });
236
+ }
237
+ function createDevSSRMiddleware(server, _pluginOptions) {
238
+ return async (req, res, next) => {
239
+ const url = req.url ?? "/";
240
+ if (url.includes(".") || url.startsWith("/node_modules/")) return next();
241
+ try {
242
+ const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
243
+ let webRequest;
244
+ try {
245
+ webRequest = await nodeToWebRequest(req);
246
+ } catch (err) {
247
+ if (err instanceof Error && err.message === "Request body too large") {
248
+ res.statusCode = 413;
249
+ res.end("Payload Too Large");
250
+ return;
251
+ }
252
+ throw err;
253
+ }
254
+ const response = await framework.handlePrachtRequest({
255
+ app: serverMod.resolvedApp,
256
+ registry: serverMod.registry,
257
+ request: webRequest,
258
+ clientEntryUrl: CLIENT_BROWSER_PATH,
259
+ apiRoutes: serverMod.apiRoutes
260
+ });
261
+ if (response.status === 404) return next();
262
+ const contentType = response.headers.get("content-type") ?? "text/html";
263
+ let body = await response.text();
264
+ if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
265
+ res.statusCode = response.status;
266
+ response.headers.forEach((value, key) => {
267
+ res.setHeader(key, value);
268
+ });
269
+ res.end(body);
270
+ } catch (error) {
271
+ if (error instanceof Error) server.ssrFixStacktrace(error);
272
+ if (req.headers["x-pracht-route-state-request"] === "1") {
273
+ res.statusCode = 500;
274
+ res.setHeader("content-type", "application/json; charset=utf-8");
275
+ res.end(JSON.stringify({ error: {
276
+ message: error instanceof Error ? error.message : String(error),
277
+ name: error instanceof Error ? error.name : "Error",
278
+ status: 500
279
+ } }));
280
+ return;
281
+ }
282
+ try {
283
+ const { buildErrorOverlayHtml } = await server.ssrLoadModule("pracht/error-overlay");
284
+ let html = buildErrorOverlayHtml({
285
+ message: error instanceof Error ? error.message : String(error),
286
+ stack: error instanceof Error ? error.stack : void 0
287
+ });
288
+ html = await server.transformIndexHtml(url, html);
289
+ res.statusCode = 500;
290
+ res.setHeader("content-type", "text/html; charset=utf-8");
291
+ res.end(html);
292
+ } catch {
293
+ next(error);
294
+ }
295
+ }
296
+ };
297
+ }
298
+ const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
299
+ async function nodeToWebRequest(req) {
300
+ const protocol = (Array.isArray(req.headers["x-forwarded-proto"]) ? req.headers["x-forwarded-proto"][0] : req.headers["x-forwarded-proto"]) ?? "http";
301
+ const host = req.headers.host ?? "localhost";
302
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
303
+ const method = req.method ?? "GET";
304
+ const headers = new Headers();
305
+ for (const [key, value] of Object.entries(req.headers)) {
306
+ if (value === void 0) continue;
307
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
308
+ else headers.set(key, value);
309
+ }
310
+ const init = {
311
+ method,
312
+ headers
313
+ };
314
+ if (!BODYLESS_METHODS.has(method.toUpperCase())) {
315
+ const MAX_BODY_SIZE = 1024 * 1024;
316
+ const chunks = [];
317
+ let totalSize = 0;
318
+ for await (const chunk of req) {
319
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
320
+ totalSize += buf.byteLength;
321
+ if (totalSize > MAX_BODY_SIZE) {
322
+ req.destroy();
323
+ throw new Error("Request body too large");
324
+ }
325
+ chunks.push(buf);
326
+ }
327
+ const body = Buffer.concat(chunks);
328
+ if (body.byteLength > 0) init.body = body;
329
+ }
330
+ return new Request(url, init);
331
+ }
332
+ function resolveOptions(options) {
333
+ return {
334
+ ...DEFAULTS,
335
+ ...options
336
+ };
337
+ }
338
+ function readClientBuildAssets(root = process.cwd()) {
339
+ const manifestPath = resolve(root, "dist/client/.vite/manifest.json");
340
+ if (!existsSync(manifestPath)) return {
341
+ clientEntryUrl: null,
342
+ cssManifest: {},
343
+ jsManifest: {}
344
+ };
345
+ const rawManifest = readFileSync(manifestPath, "utf-8");
346
+ const manifest = JSON.parse(rawManifest);
347
+ const clientEntry = manifest[PRACHT_CLIENT_MODULE_ID];
348
+ function collectTransitiveDeps(key) {
349
+ const css = /* @__PURE__ */ new Set();
350
+ const js = /* @__PURE__ */ new Set();
351
+ const visited = /* @__PURE__ */ new Set();
352
+ function collect(k) {
353
+ if (visited.has(k)) return;
354
+ visited.add(k);
355
+ const entry = manifest[k];
356
+ if (!entry) return;
357
+ for (const c of entry.css ?? []) css.add(c);
358
+ js.add(entry.file);
359
+ for (const imp of entry.imports ?? []) collect(imp);
360
+ }
361
+ collect(key);
362
+ return {
363
+ css: [...css],
364
+ js: [...js]
365
+ };
366
+ }
367
+ const cssManifest = {};
368
+ const jsManifest = {};
369
+ for (const [key, entry] of Object.entries(manifest)) {
370
+ if (!entry.src) continue;
371
+ const deps = collectTransitiveDeps(key);
372
+ if (deps.css.length > 0) cssManifest[key] = deps.css.map((f) => `/${f}`);
373
+ if (deps.js.length > 0) jsManifest[key] = deps.js.map((f) => `/${f}`);
374
+ }
375
+ return {
376
+ clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
377
+ cssManifest,
378
+ jsManifest
379
+ };
380
+ }
381
+ //#endregion
382
+ export { PRACHT_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, createPrachtClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, pracht };
@@ -0,0 +1,23 @@
1
+ //#region src/pages-router.d.ts
2
+ interface ScannedPage {
3
+ absolutePath: string;
4
+ relativePath: string;
5
+ routePath: string;
6
+ isIndex: boolean;
7
+ isCatchAll: boolean;
8
+ isDynamic: boolean;
9
+ renderMode?: string;
10
+ }
11
+ interface PagesRouterOptions {
12
+ pagesDir: string;
13
+ pagesDefaultRender?: string;
14
+ }
15
+ declare function scanPagesDirectory(pagesDir: string): ScannedPage[];
16
+ declare function filePathToRoutePath(relativePath: string): string;
17
+ declare function sortRoutes(pages: ScannedPage[]): ScannedPage[];
18
+ declare function generatePagesManifestSource(pages: ScannedPage[], options: PagesRouterOptions & {
19
+ pagesDirPrefix?: string;
20
+ }): string;
21
+ declare function generateRoutesFile(pagesDir: string, outputPath: string, options: PagesRouterOptions): void;
22
+ //#endregion
23
+ export { PagesRouterOptions, ScannedPage, filePathToRoutePath, generatePagesManifestSource, generateRoutesFile, scanPagesDirectory, sortRoutes };
@@ -0,0 +1,128 @@
1
+ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, extname, join, relative } from "node:path";
3
+ //#region src/pages-router.ts
4
+ const PAGE_EXTENSIONS = new Set([
5
+ ".tsx",
6
+ ".ts",
7
+ ".jsx",
8
+ ".js"
9
+ ]);
10
+ function scanPagesDirectory(pagesDir) {
11
+ const pages = [];
12
+ scan(pagesDir, pagesDir, pages);
13
+ return sortRoutes(pages);
14
+ }
15
+ function scan(dir, root, pages) {
16
+ let entries;
17
+ try {
18
+ entries = readdirSync(dir);
19
+ } catch {
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ const abs = join(dir, entry);
24
+ if (statSync(abs).isDirectory()) {
25
+ scan(abs, root, pages);
26
+ continue;
27
+ }
28
+ const ext = extname(entry);
29
+ if (!PAGE_EXTENSIONS.has(ext)) continue;
30
+ const name = basename(entry, ext);
31
+ if (name.startsWith("_") && name !== "_app") continue;
32
+ const rel = relative(root, abs);
33
+ const routePath = filePathToRoutePath(rel);
34
+ const renderMode = extractRenderMode(readFileSync(abs, "utf-8"));
35
+ pages.push({
36
+ absolutePath: abs,
37
+ relativePath: rel,
38
+ routePath,
39
+ isIndex: name === "index",
40
+ isCatchAll: name.startsWith("[..."),
41
+ isDynamic: name.startsWith("[") && !name.startsWith("[..."),
42
+ renderMode
43
+ });
44
+ }
45
+ }
46
+ function filePathToRoutePath(relativePath) {
47
+ let route = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "");
48
+ route = route.replace(/\\/g, "/");
49
+ if (route === "_app" || route.endsWith("/_app")) return "__shell__";
50
+ if (route === "index") return "/";
51
+ route = route.replace(/\/index$/, "");
52
+ route = route.replace(/\[([^\].]+)\]/g, ":$1");
53
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*");
54
+ return `/${route}`;
55
+ }
56
+ function sortRoutes(pages) {
57
+ return [...pages].filter((p) => p.routePath !== "__shell__").sort((a, b) => {
58
+ if (a.isCatchAll && !b.isCatchAll) return 1;
59
+ if (!a.isCatchAll && b.isCatchAll) return -1;
60
+ if (a.isDynamic && !b.isDynamic) return 1;
61
+ if (!a.isDynamic && b.isDynamic) return -1;
62
+ return a.routePath.localeCompare(b.routePath);
63
+ });
64
+ }
65
+ const RENDER_MODE_RE = /export\s+const\s+RENDER_MODE\s*=\s*["'](\w+)["']/;
66
+ function extractRenderMode(source) {
67
+ const match = RENDER_MODE_RE.exec(source);
68
+ return match ? match[1] : void 0;
69
+ }
70
+ function generatePagesManifestSource(pages, options) {
71
+ const pagesDir = options.pagesDir;
72
+ const defaultRender = options.pagesDefaultRender ?? "ssr";
73
+ const prefix = options.pagesDirPrefix;
74
+ const appFile = scanAllFiles(pagesDir).find((f) => basename(f, extname(f)) === "_app" && PAGE_EXTENSIONS.has(extname(f)));
75
+ const lines = ["import { defineApp, group, route } from \"@pracht/core\";", ""];
76
+ const routeEntries = [];
77
+ for (const page of pages) {
78
+ const render = page.renderMode ?? defaultRender;
79
+ const filePath = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
80
+ routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${JSON.stringify(filePath)}, { render: ${JSON.stringify(render)} })`);
81
+ }
82
+ if (appFile) {
83
+ const appPath = prefix ? `${prefix}/_app.${extname(appFile).slice(1)}` : `./${relative(join(pagesDir, ".."), appFile).replace(/\\/g, "/")}`;
84
+ lines.push("const app = defineApp({");
85
+ lines.push(" shells: {");
86
+ lines.push(` pages: ${JSON.stringify(appPath)},`);
87
+ lines.push(" },");
88
+ lines.push(" routes: [");
89
+ lines.push(` group({ shell: "pages" }, [`);
90
+ lines.push(routeEntries.join(",\n"));
91
+ lines.push(" ]),");
92
+ lines.push(" ],");
93
+ lines.push("});");
94
+ } else {
95
+ lines.push("const app = defineApp({");
96
+ lines.push(" routes: [");
97
+ lines.push(routeEntries.join(",\n"));
98
+ lines.push(" ],");
99
+ lines.push("});");
100
+ }
101
+ lines.push("");
102
+ return lines.join("\n");
103
+ }
104
+ function scanAllFiles(dir) {
105
+ const results = [];
106
+ let entries;
107
+ try {
108
+ entries = readdirSync(dir);
109
+ } catch {
110
+ return results;
111
+ }
112
+ for (const entry of entries) {
113
+ const abs = join(dir, entry);
114
+ if (statSync(abs).isDirectory()) results.push(...scanAllFiles(abs));
115
+ else results.push(abs);
116
+ }
117
+ return results;
118
+ }
119
+ function generateRoutesFile(pagesDir, outputPath, options) {
120
+ writeFileSync(outputPath, [
121
+ "// Auto-generated from pages/ directory by @pracht/vite-plugin.",
122
+ "// Customize this file and remove `pagesDir` from pracht config to use it directly.",
123
+ "",
124
+ generatePagesManifestSource(scanPagesDirectory(pagesDir), options).replace("const app = defineApp(", "export const app = defineApp(")
125
+ ].join("\n"), "utf-8");
126
+ }
127
+ //#endregion
128
+ export { filePathToRoutePath, generatePagesManifestSource, generateRoutesFile, scanPagesDirectory, sortRoutes };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@pracht/vite-plugin",
3
+ "version": "0.0.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "./pages-router": {
14
+ "types": "./dist/pages-router.d.mts",
15
+ "default": "./dist/pages-router.mjs"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@preact/preset-vite": "^2.10.5",
20
+ "@pracht/adapter-cloudflare": "0.0.0",
21
+ "@pracht/core": "0.0.0",
22
+ "@pracht/adapter-vercel": "0.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@cloudflare/vite-plugin": "^1.0.0",
26
+ "vite": "^8.0.0",
27
+ "wrangler": "^4.81.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@cloudflare/vite-plugin": {
31
+ "optional": true
32
+ },
33
+ "wrangler": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown"
39
+ }
40
+ }