vite-plugin-kiru 0.29.9 → 0.29.11

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/README.md CHANGED
@@ -1,14 +1,6 @@
1
1
  # vite-plugin-kiru
2
2
 
3
- Vite plugin for <a href="https://kirujs.dev">Kiru</a> apps that enables HMR, devtools, file-based routing, and SSG/SSR.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm i -D vite-plugin-kiru
9
- # or
10
- pnpm add -D vite-plugin-kiru
11
- ```
3
+ Vite plugin for <a href="https://kirujs.dev">Kiru</a> apps that enables HMR, devtools, and SSG with file-based routing & sitemap generation.
12
4
 
13
5
  ## Basic Usage
14
6
 
@@ -60,25 +52,106 @@ kiru({
60
52
  onFileExcluded: (id) => {
61
53
  console.log(`Excluded: ${id}`)
62
54
  },
55
+
56
+ // Static Site Generation (SSG) configuration
57
+ ssg: {
58
+ // Base URL for the app
59
+ baseUrl: "/",
60
+ // Directory containing pages
61
+ dir: "./src/pages",
62
+ // Document component filename pattern
63
+ document: "document.{tsx,jsx}",
64
+ // Page component filename pattern
65
+ page: "index.{tsx,jsx}",
66
+ // Layout component filename pattern
67
+ layout: "layout.{tsx,jsx}",
68
+ // Enable view transitions for route changes and page loaders
69
+ transition: true,
70
+ // Build options
71
+ build: {
72
+ // Maximum number of pages to render concurrently
73
+ maxConcurrentRenders: 100,
74
+ },
75
+ // Sitemap generation options
76
+ sitemap: {
77
+ // Domain for sitemap URLs (required)
78
+ domain: "https://example.com",
79
+ // Default last modified date for all URLs
80
+ lastmod: new Date(),
81
+ // Default change frequency (hourly | daily | weekly | monthly | yearly | never)
82
+ changefreq: "weekly", // default: "weekly"
83
+ // Default priority (0.0 to 1.0)
84
+ priority: 0.5, // default: 0.5
85
+ // Per-route overrides
86
+ overrides: {
87
+ "/": {
88
+ changefreq: "daily",
89
+ priority: 0.8,
90
+ lastmod: new Date("2024-01-01"),
91
+ // Images to include for this route
92
+ images: ["/images/hero.png"],
93
+ // Videos to include for this route
94
+ videos: [
95
+ {
96
+ title: "Product Demo",
97
+ thumbnail_loc: "/images/video-thumbnail.png",
98
+ description: "A demonstration of our product features.",
99
+ },
100
+ ],
101
+ },
102
+ "/blog": {
103
+ changefreq: "weekly",
104
+ priority: 0.7,
105
+ },
106
+ },
107
+ },
108
+ },
109
+
110
+ // Or, if you want to enable SSG with default configuration:
111
+ ssg: true,
112
+ // (this will not include sitemap generation as it requires manual configuration)
63
113
  })
64
114
  ```
65
115
 
116
+ ## Static Site Generation (SSG)
117
+
118
+ The plugin supports static site generation with configurable sitemap creation. When SSG is enabled, all routes are pre-rendered at build time and a `sitemap.xml` file is generated if configured.
119
+
120
+ ### Sitemap Generation
121
+
122
+ The sitemap feature generates a `sitemap.xml` file in your build output with all discovered routes (excluding 404 pages).
123
+
124
+ **Features:**
125
+
126
+ - Automatic route discovery from your file structure
127
+ - Configurable default `changefreq`, `priority`, and `lastmod` for all routes
128
+ - Per-route overrides for fine-grained control
129
+ - Support for images and videos (Google sitemap extensions)
130
+
131
+ **Example:**
132
+
133
+ ```ts
134
+ ssg: {
135
+ sitemap: {
136
+ domain: "https://kirujs.dev",
137
+ changefreq: "weekly",
138
+ priority: 0.5,
139
+ overrides: {
140
+ "/": {
141
+ changefreq: "daily",
142
+ priority: 0.8,
143
+ images: ["/images/kiru.png"],
144
+ },
145
+ },
146
+ },
147
+ }
148
+ ```
149
+
150
+ This will generate a `sitemap.xml` file in `dist/client/sitemap.xml` with all your routes properly formatted.
151
+
66
152
  ## Features
67
153
 
68
- - **File-based routing**: Automatic route generation from your pages directory
69
- - **SSR/SSG**: Server-side rendering and static site generation
154
+ - **SSG + file-based routing**: Automatic route discovery, static site and sitemap generation
70
155
  - **HMR**: Hot module replacement for fast development
71
156
  - **Devtools**: Built-in development tools for debugging
72
157
  - **TypeScript**: Full TypeScript support with proper type definitions
73
-
74
- ## Architecture
75
-
76
- The plugin is organized into focused modules:
77
-
78
- - `virtual-modules.ts` - Virtual module generation for routing
79
- - `dev-server.ts` - Development server SSR handling
80
- - `preview-server.ts` - Preview server middleware
81
- - `devtools.ts` - Development tools integration
82
- - `ssg.ts` - Static site generation
83
- - `config.ts` - Configuration management
84
- - `utils.ts` - Shared utilities
package/dist/index.d.ts CHANGED
@@ -10,6 +10,86 @@ export interface SSGBuildOptions {
10
10
  maxConcurrentRenders?: number
11
11
  }
12
12
 
13
+ export type SSGSitemapChangefreq =
14
+ // | "always"
15
+ "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"
16
+
17
+ export interface SSGSitemapVideo {
18
+ title: string
19
+ thumbnail_loc: string
20
+ description?: string
21
+ }
22
+
23
+ export interface SSGSitemapOverride {
24
+ /**
25
+ * Change frequency override for this route
26
+ * @default "weekly"
27
+ */
28
+ changefreq?: SSGSitemapChangefreq
29
+ /**
30
+ * Priority override for this route (0.0 to 1.0)
31
+ * @default 0.5
32
+ */
33
+ priority?: number
34
+ /**
35
+ * Last modified date override for this route
36
+ */
37
+ lastmod?: Date
38
+ /**
39
+ * Images to include for this route
40
+ * @example ["/images/kiru.png"]
41
+ */
42
+ images?: string[]
43
+ /**
44
+ * Videos to include for this route
45
+ * @example
46
+ * ```ts
47
+ * videos: [
48
+ * {
49
+ * title: "Kiru",
50
+ * thumbnail_loc: "/images/kiru.png",
51
+ * description: "Kiru is a framework for building web applications."
52
+ * }
53
+ * ]
54
+ * ```
55
+ */
56
+ videos?: Array<SSGSitemapVideo>
57
+ }
58
+
59
+ export interface SSGSitemapOptions {
60
+ /**
61
+ * The domain to use for sitemap URLs
62
+ * @example "https://example.com"
63
+ */
64
+ domain: string
65
+ /**
66
+ * Default last modified date for all URLs
67
+ */
68
+ lastmod?: Date
69
+ /**
70
+ * Default change frequency for all URLs
71
+ * @default "weekly"
72
+ */
73
+ changefreq?: SSGSitemapChangefreq
74
+ /**
75
+ * Default priority for all URLs (0.0 to 1.0)
76
+ * @default 0.5
77
+ */
78
+ priority?: number
79
+ /**
80
+ * Per-route overrides for sitemap entries
81
+ * @example
82
+ * ```ts
83
+ * overrides: {
84
+ * "/": {
85
+ * changefreq: "never",
86
+ * priority: 0.8,
87
+ * },
88
+ * }
89
+ */
90
+ overrides?: Record<string, SSGSitemapOverride>
91
+ }
92
+
13
93
  export interface SSGOptions {
14
94
  /**
15
95
  * The base URL of the app
@@ -42,6 +122,11 @@ export interface SSGOptions {
42
122
  */
43
123
  transition?: boolean
44
124
 
125
+ /**
126
+ * Options for sitemap generation
127
+ */
128
+ sitemap?: SSGSitemapOptions
129
+
45
130
  /**
46
131
  * Options for build
47
132
  */
package/dist/index.js CHANGED
@@ -8810,11 +8810,10 @@ function createLogger(state) {
8810
8810
  }
8811
8811
  function resolveUserDocument(projectRoot, ssgOptions) {
8812
8812
  const { dir, document } = ssgOptions;
8813
- const matches = globSync(path2.resolve(dir, document), {
8814
- cwd: projectRoot
8815
- });
8813
+ const fp = path2.resolve(projectRoot, dir, document).replace(/\\/g, "/");
8814
+ const matches = globSync(fp);
8816
8815
  if (!matches.length) {
8817
- throw new Error(`Document not found`);
8816
+ throw new Error(`Document not found at ${fp}`);
8818
8817
  }
8819
8818
  return path2.resolve(projectRoot, matches[0]).replace(/\\/g, "/");
8820
8819
  }
@@ -8969,6 +8968,7 @@ function createPluginState(opts = {}) {
8969
8968
  page: ssg.page ?? page,
8970
8969
  layout: ssg.layout ?? layout,
8971
8970
  transition: ssg.transition ?? transition,
8971
+ sitemap: ssg.sitemap,
8972
8972
  build: {
8973
8973
  maxConcurrentRenders: ssg.build?.maxConcurrentRenders ?? maxConcurrentRenders
8974
8974
  }
@@ -11213,7 +11213,7 @@ import path6 from "node:path";
11213
11213
  import fs3 from "node:fs";
11214
11214
  import { pathToFileURL } from "node:url";
11215
11215
  async function generateStaticSite(state, outputOptions, bundle, log) {
11216
- const { projectRoot, baseOutDir, manifestPath } = state;
11216
+ const { projectRoot, baseOutDir, manifestPath, ssgOptions } = state;
11217
11217
  const outDirAbs = path6.resolve(projectRoot, outputOptions?.dir ?? "dist");
11218
11218
  const ssrEntry = Object.values(bundle).find(
11219
11219
  (c) => c.type === "chunk" && c.isEntry
@@ -11235,7 +11235,7 @@ async function generateStaticSite(state, outputOptions, bundle, log) {
11235
11235
  const routes = Object.keys(paths);
11236
11236
  log(ANSI.cyan("[SSG]"), `discovered ${routes.length} routes:`, routes);
11237
11237
  const renderingChunks = [];
11238
- const maxConcurrentRenders = state.ssgOptions.build.maxConcurrentRenders;
11238
+ const maxConcurrentRenders = ssgOptions.build.maxConcurrentRenders;
11239
11239
  for (let i = 0; i < routes.length; i += maxConcurrentRenders) {
11240
11240
  const chunkKeys = routes.slice(i, i + maxConcurrentRenders);
11241
11241
  renderingChunks.push(
@@ -11254,8 +11254,7 @@ async function generateStaticSite(state, outputOptions, bundle, log) {
11254
11254
  route,
11255
11255
  srcFilePath,
11256
11256
  clientEntry,
11257
- manifest,
11258
- log
11257
+ manifest
11259
11258
  );
11260
11259
  const filePath = getOutputPath(clientOutDirAbs, route);
11261
11260
  log(ANSI.cyan("[SSG]"), "write:", ANSI.black(filePath));
@@ -11265,6 +11264,9 @@ async function generateStaticSite(state, outputOptions, bundle, log) {
11265
11264
  );
11266
11265
  }
11267
11266
  await appendStaticPropsToClientModules(state, clientOutDirAbs, log);
11267
+ if (ssgOptions.sitemap?.domain) {
11268
+ await generateSitemap(state, routes, clientOutDirAbs, log);
11269
+ }
11268
11270
  }
11269
11271
  async function getClientAssets(clientOutDirAbs, manifestPath) {
11270
11272
  let clientEntry = null;
@@ -11350,7 +11352,7 @@ function findClientEntry(dir) {
11350
11352
  }
11351
11353
  return null;
11352
11354
  }
11353
- async function renderRoute(state, mod, route, srcFilePath, clientEntry, manifest, log) {
11355
+ async function renderRoute(state, mod, route, srcFilePath, clientEntry, manifest) {
11354
11356
  const moduleIds = [];
11355
11357
  const { projectRoot, ssgOptions } = state;
11356
11358
  const documentPath = path6.resolve(
@@ -11371,7 +11373,6 @@ async function renderRoute(state, mod, route, srcFilePath, clientEntry, manifest
11371
11373
  };
11372
11374
  const result = await mod.render(route, ctx);
11373
11375
  let html = result.body;
11374
- log(ANSI.cyan("[SSG]"), ` Total modules tracked: ${moduleIds.length}`);
11375
11376
  let cssLinks = "";
11376
11377
  if (manifest) {
11377
11378
  cssLinks = collectCssForModules(manifest, moduleIds, projectRoot);
@@ -11398,6 +11399,101 @@ function getOutputPath(clientOutDirAbs, route) {
11398
11399
  }
11399
11400
  return path6.resolve(dirPath, `${last}.html`);
11400
11401
  }
11402
+ async function generateSitemap(state, routes, clientOutDirAbs, log) {
11403
+ const sitemapConfig = state.ssgOptions.sitemap;
11404
+ const {
11405
+ domain,
11406
+ lastmod: lastModified,
11407
+ changefreq = "weekly",
11408
+ priority = 0.5,
11409
+ overrides = {}
11410
+ } = sitemapConfig;
11411
+ const baseUrl = state.ssgOptions.baseUrl;
11412
+ const normalizedDomain = domain.replace(/\/$/, "");
11413
+ const normalizedBaseUrl = baseUrl === "/" ? "" : baseUrl.replace(/\/$/, "");
11414
+ let hasImages = false;
11415
+ let hasVideos = false;
11416
+ for (const route of routes) {
11417
+ const override = overrides[route];
11418
+ if (override?.images?.length) {
11419
+ hasImages = true;
11420
+ }
11421
+ if (override?.videos?.length) {
11422
+ hasVideos = true;
11423
+ }
11424
+ }
11425
+ const sortedRoutes = [...routes].filter((route) => !route.endsWith("/404")).sort((a, b) => {
11426
+ if (a === "/") return -1;
11427
+ if (b === "/") return 1;
11428
+ const aDepth = a.split("/").filter(Boolean).length;
11429
+ const bDepth = b.split("/").filter(Boolean).length;
11430
+ if (aDepth !== bDepth) {
11431
+ return aDepth - bDepth;
11432
+ }
11433
+ return a < b ? -1 : a > b ? 1 : 0;
11434
+ });
11435
+ const urls = sortedRoutes.map((route) => {
11436
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
11437
+ const url = `${normalizedDomain}${normalizedBaseUrl}${normalizedRoute}`;
11438
+ const override = overrides[route] || {};
11439
+ const routeChangefreq = override.changefreq ?? changefreq;
11440
+ const routePriority = override.priority ?? priority;
11441
+ const routeLastModified = override.lastmod ?? lastModified;
11442
+ let urlEntry = ` <url>
11443
+ <loc>${escapeXml(url)}</loc>
11444
+ <changefreq>${routeChangefreq}</changefreq>
11445
+ <priority>${routePriority}</priority>`;
11446
+ if (routeLastModified) {
11447
+ const lastMod = routeLastModified.toISOString().split("T")[0];
11448
+ urlEntry += `
11449
+ <lastmod>${lastMod}</lastmod>`;
11450
+ }
11451
+ if (override.images?.length) {
11452
+ for (const image of override.images) {
11453
+ const imageUrl = image.startsWith("http") ? image : `${normalizedDomain}${normalizedBaseUrl}${image.startsWith("/") ? image : `/${image}`}`;
11454
+ urlEntry += `
11455
+ <image:image>
11456
+ <image:loc>${escapeXml(imageUrl)}</image:loc>
11457
+ </image:image>`;
11458
+ }
11459
+ }
11460
+ if (override.videos?.length) {
11461
+ for (const video of override.videos) {
11462
+ const thumbnailUrl = video.thumbnail_loc.startsWith("http") ? video.thumbnail_loc : `${normalizedDomain}${normalizedBaseUrl}${video.thumbnail_loc.startsWith("/") ? video.thumbnail_loc : `/${video.thumbnail_loc}`}`;
11463
+ urlEntry += `
11464
+ <video:video>
11465
+ <video:title>${escapeXml(video.title)}</video:title>
11466
+ <video:thumbnail_loc>${escapeXml(thumbnailUrl)}</video:thumbnail_loc>`;
11467
+ if (video.description) {
11468
+ urlEntry += `
11469
+ <video:description>${escapeXml(
11470
+ video.description
11471
+ )}</video:description>`;
11472
+ }
11473
+ urlEntry += `
11474
+ </video:video>`;
11475
+ }
11476
+ }
11477
+ urlEntry += `
11478
+ </url>`;
11479
+ return urlEntry;
11480
+ }).join("\n");
11481
+ const namespaces = [
11482
+ 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
11483
+ hasImages && 'xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"',
11484
+ hasVideos && 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"'
11485
+ ].filter(Boolean).join(" ");
11486
+ const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
11487
+ <urlset ${namespaces}>
11488
+ ${urls}
11489
+ </urlset>`;
11490
+ const sitemapPath = path6.resolve(clientOutDirAbs, "sitemap.xml");
11491
+ fs3.writeFileSync(sitemapPath, sitemapXml, "utf-8");
11492
+ log(ANSI.cyan("[SSG]"), "Generated sitemap:", ANSI.black(sitemapPath));
11493
+ }
11494
+ function escapeXml(unsafe) {
11495
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
11496
+ }
11401
11497
  async function appendStaticPropsToClientModules(state, clientOutDirAbs, log) {
11402
11498
  const { projectRoot, manifestPath, ssgOptions } = state;
11403
11499
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-kiru",
3
- "version": "0.29.9",
3
+ "version": "0.29.11",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -17,14 +17,14 @@
17
17
  "rollup": "^4.46.2",
18
18
  "tsx": "^4.20.3",
19
19
  "typescript": "^5.9.2",
20
- "kiru-devtools-host": "^1.0.0",
21
- "kiru-devtools-client": "^0.0.0"
20
+ "kiru-devtools-client": "^0.0.0",
21
+ "kiru-devtools-host": "^1.0.0"
22
22
  },
23
23
  "dependencies": {
24
24
  "glob": "^11.0.3",
25
25
  "magic-string": "^0.30.17",
26
26
  "mime": "^4.1.0",
27
- "vite": "^7.0.6"
27
+ "vite": "^7.2.2"
28
28
  },
29
29
  "scripts": {
30
30
  "test": "echo \"Error: no test specified\"",