@onruntime/next-sitemap 0.6.2 → 0.8.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.
package/dist/app/index.js CHANGED
@@ -1,9 +1,168 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { createJiti } from 'jiti';
4
- import stripJsonComments from 'strip-json-comments';
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import { dirname, join, isAbsolute, relative, delimiter } from 'path';
3
+ import * as childProcess from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
5
 
6
6
  // src/app/index.ts
7
+ var NO_STATIC_PARAMS = "NO_STATIC_PARAMS";
8
+ var spawnProcess = childProcess.spawn;
9
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
10
+ var paramsCache = /* @__PURE__ */ new Map();
11
+ var isDev = process.env.NODE_ENV === "development";
12
+ async function executeWorker(directory, fileKey, debug) {
13
+ const absolutePath = join(directory, fileKey.replace("./", ""));
14
+ const projectRoot = process.cwd();
15
+ const distRoot = join(__dirname$1, "..");
16
+ const workerPath = join(distRoot, "worker.cjs");
17
+ const loaderPath = join(distRoot, "loader.js");
18
+ if (debug) {
19
+ console.log(`[next-sitemap] Worker path: ${workerPath}`);
20
+ console.log(`[next-sitemap] Worker exists: ${existsSync(workerPath)}`);
21
+ console.log(`[next-sitemap] Loader path: ${loaderPath}`);
22
+ console.log(`[next-sitemap] Loader exists: ${existsSync(loaderPath)}`);
23
+ }
24
+ return new Promise((resolve) => {
25
+ const nodePath = [
26
+ join(projectRoot, "node_modules"),
27
+ join(__dirname$1, "..", "node_modules")
28
+ ].join(delimiter);
29
+ const importFlag = ["--import", loaderPath];
30
+ const args = [...importFlag, workerPath, absolutePath, projectRoot];
31
+ const child = spawnProcess("node", args, {
32
+ cwd: projectRoot,
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ env: { ...process.env, NODE_PATH: nodePath }
35
+ });
36
+ let stdout = "";
37
+ let stderr = "";
38
+ child.stdout.on("data", (data) => {
39
+ stdout += data.toString();
40
+ });
41
+ child.stderr.on("data", (data) => {
42
+ stderr += data.toString();
43
+ });
44
+ child.on("close", (code) => {
45
+ if (debug && stderr) {
46
+ console.warn(`[next-sitemap] Worker stderr: ${stderr}`);
47
+ }
48
+ if (code !== 0 && code !== null) {
49
+ if (debug) console.warn(`[next-sitemap] Worker exited with code ${code}`);
50
+ resolve(null);
51
+ return;
52
+ }
53
+ try {
54
+ const lines = stdout.trim().split("\n");
55
+ const result = JSON.parse(lines[lines.length - 1]);
56
+ if (result.success) {
57
+ resolve(result.params);
58
+ } else {
59
+ if (result.error !== NO_STATIC_PARAMS && debug) {
60
+ console.warn(`[next-sitemap] Worker error: ${result.error}`);
61
+ }
62
+ resolve(null);
63
+ }
64
+ } catch {
65
+ if (debug) console.warn(`[next-sitemap] Failed to parse worker output: ${stdout}`);
66
+ resolve(null);
67
+ }
68
+ });
69
+ child.on("error", (err) => {
70
+ if (debug) console.warn(`[next-sitemap] Failed to spawn worker: ${err.message}`);
71
+ resolve(null);
72
+ });
73
+ });
74
+ }
75
+ async function getRouteParams(route, directory, debug) {
76
+ const cacheKey = route.fileKey;
77
+ if (paramsCache.has(cacheKey)) {
78
+ return paramsCache.get(cacheKey);
79
+ }
80
+ if (debug) {
81
+ console.log(`[next-sitemap] ${route.pathname}: executing static params via worker`);
82
+ }
83
+ const params = await executeWorker(directory, route.fileKey, debug);
84
+ paramsCache.set(cacheKey, params);
85
+ if (debug && params) {
86
+ console.log(`[next-sitemap] ${route.pathname}: got ${params.length} params`);
87
+ }
88
+ return params;
89
+ }
90
+ async function generateAllPaths(routes, directory, debug) {
91
+ const staticPaths = ["/"];
92
+ const dynamicRoutes = [];
93
+ for (const route of routes) {
94
+ if (route.dynamicSegments.length === 0) {
95
+ if (route.pathname !== "/") {
96
+ staticPaths.push(route.pathname);
97
+ }
98
+ } else {
99
+ dynamicRoutes.push(route);
100
+ }
101
+ }
102
+ const startTime = isDev ? Date.now() : 0;
103
+ if (isDev && dynamicRoutes.length > 0) {
104
+ console.log(`[next-sitemap] Generating sitemap for ${dynamicRoutes.length} dynamic routes (dev only, instant in production)...`);
105
+ }
106
+ const dynamicResults = await Promise.all(
107
+ dynamicRoutes.map(async (route) => {
108
+ const params = await getRouteParams(route, directory, debug);
109
+ if (!params || params.length === 0) {
110
+ if (debug) {
111
+ console.warn(`[next-sitemap] Skipping dynamic route ${route.pathname}: no static params or empty result.`);
112
+ }
113
+ return [];
114
+ }
115
+ const paths = [];
116
+ for (const param of params) {
117
+ let path = route.pathname;
118
+ let valid = true;
119
+ for (const segment of route.dynamicSegments) {
120
+ const value = param[segment];
121
+ if (value === void 0) {
122
+ if (debug) {
123
+ console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
124
+ }
125
+ valid = false;
126
+ break;
127
+ }
128
+ path = path.replace(`[${segment}]`, value);
129
+ }
130
+ if (valid) paths.push(path);
131
+ }
132
+ return paths;
133
+ })
134
+ );
135
+ const allPaths = new Set(staticPaths);
136
+ for (const paths of dynamicResults) {
137
+ for (const path of paths) {
138
+ allPaths.add(path);
139
+ }
140
+ }
141
+ if (isDev && dynamicRoutes.length > 0) {
142
+ const elapsed = Date.now() - startTime;
143
+ console.log(`[next-sitemap] Done! Found ${allPaths.size} total URLs in ${elapsed}ms.`);
144
+ }
145
+ return Array.from(allPaths);
146
+ }
147
+ function pathsToEntries(paths, config) {
148
+ const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
149
+ return paths.filter((pathname) => !shouldExclude(pathname, exclude)).map((pathname) => {
150
+ const entry = {
151
+ url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
152
+ lastModified: /* @__PURE__ */ new Date(),
153
+ changeFrequency: getChangeFreq(pathname, changeFreq),
154
+ priority: getPriority(pathname, priority)
155
+ };
156
+ if (locales.length > 0) {
157
+ entry.alternates = {
158
+ languages: Object.fromEntries(
159
+ locales.map((locale) => [locale, buildUrl(baseUrl, pathname, locale, defaultLocale)])
160
+ )
161
+ };
162
+ }
163
+ return entry;
164
+ });
165
+ }
7
166
 
8
167
  // src/index.ts
9
168
  function calculateDepthPriority(pathname) {
@@ -52,7 +211,8 @@ function buildUrl(baseUrl, pathname, locale, defaultLocale) {
52
211
  }
53
212
  return `${baseUrl}/${locale}${normalizedPath}`;
54
213
  }
55
- function generateSitemapXml(entries) {
214
+ function generateSitemapXml(entries, options) {
215
+ const { poweredBy = true } = options || {};
56
216
  const hasAlternates = entries.some((e) => e.alternates?.languages);
57
217
  const xmlns = hasAlternates ? 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"' : 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
58
218
  const urlEntries = entries.map((entry) => {
@@ -76,13 +236,14 @@ function generateSitemapXml(entries) {
76
236
  ${parts.join("\n")}
77
237
  </url>`;
78
238
  }).join("\n");
79
- return `<?xml version="1.0" encoding="UTF-8"?>
239
+ const comment = poweredBy ? "\n<!-- Powered by @onruntime/next-sitemap -->" : "";
240
+ return `<?xml version="1.0" encoding="UTF-8"?>${comment}
80
241
  <urlset ${xmlns}>
81
242
  ${urlEntries}
82
243
  </urlset>`;
83
244
  }
84
245
  function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
85
- const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [] } = options || {};
246
+ const { sitemapPattern = "/sitemap-{id}.xml", additionalSitemaps = [], poweredBy = true } = options || {};
86
247
  const now = (/* @__PURE__ */ new Date()).toISOString();
87
248
  const paginatedEntries = Array.from({ length: sitemapCount }, (_, i) => {
88
249
  const loc = `${baseUrl}${sitemapPattern.replace("{id}", String(i))}`;
@@ -99,24 +260,24 @@ function generateSitemapIndexXml(baseUrl, sitemapCount, options) {
99
260
  </sitemap>`;
100
261
  });
101
262
  const allEntries = [...paginatedEntries, ...additionalEntries].join("\n");
102
- return `<?xml version="1.0" encoding="UTF-8"?>
263
+ const comment = poweredBy ? "\n<!-- Powered by @onruntime/next-sitemap -->" : "";
264
+ return `<?xml version="1.0" encoding="UTF-8"?>${comment}
103
265
  <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
104
266
  ${allEntries}
105
267
  </sitemapindex>`;
106
268
  }
107
269
 
108
270
  // src/app/index.ts
109
- var joinPath = (...segments) => path.join(...segments);
110
271
  function findPageFiles(dir, baseDir = dir) {
111
272
  const files = [];
112
273
  try {
113
- const entries = fs.readdirSync(dir, { withFileTypes: true });
274
+ const entries = readdirSync(dir, { withFileTypes: true });
114
275
  for (const entry of entries) {
115
- const fullPath = path.join(dir, entry.name);
276
+ const fullPath = join(dir, entry.name);
116
277
  if (entry.isDirectory()) {
117
278
  files.push(...findPageFiles(fullPath, baseDir));
118
279
  } else if (/^page\.(tsx?|jsx?)$/.test(entry.name)) {
119
- const relativePath = "./" + path.relative(baseDir, fullPath).replace(/\\/g, "/");
280
+ const relativePath = "./" + relative(baseDir, fullPath).replace(/\\/g, "/");
120
281
  files.push(relativePath);
121
282
  }
122
283
  }
@@ -124,83 +285,12 @@ function findPageFiles(dir, baseDir = dir) {
124
285
  }
125
286
  return files;
126
287
  }
127
- function detectAppDirectory() {
128
- const srcApp = path.join(process.cwd(), "src/app");
129
- if (fs.existsSync(srcApp)) {
130
- return srcApp;
131
- }
132
- return path.join(process.cwd(), "app");
133
- }
134
- function resolveAppDirectory(options) {
135
- if (options.appDirectory) {
136
- return path.isAbsolute(options.appDirectory) ? options.appDirectory : path.join(process.cwd(), options.appDirectory);
288
+ function resolveAppDirectory(appDirectory) {
289
+ if (appDirectory) {
290
+ return isAbsolute(appDirectory) ? appDirectory : join(process.cwd(), appDirectory);
137
291
  }
138
- return detectAppDirectory();
139
- }
140
- function getPageKeys(options) {
141
- return findPageFiles(resolveAppDirectory(options));
142
- }
143
- var jitiCache = /* @__PURE__ */ new Map();
144
- function getTsconfigPaths(projectRoot, debug = false) {
145
- const alias = {};
146
- try {
147
- const tsconfigPath = path.join(projectRoot, "tsconfig.json");
148
- if (debug) {
149
- console.log("[next-sitemap] Looking for tsconfig at:", tsconfigPath);
150
- console.log("[next-sitemap] tsconfig exists:", fs.existsSync(tsconfigPath));
151
- }
152
- if (fs.existsSync(tsconfigPath)) {
153
- const content = fs.readFileSync(tsconfigPath, "utf-8");
154
- const withoutComments = stripJsonComments(content);
155
- const cleaned = withoutComments.replace(/,(\s*[}\]])/g, "$1");
156
- if (debug) {
157
- console.log("[next-sitemap] Cleaned tsconfig (first 500 chars):", cleaned.slice(0, 500));
158
- }
159
- const tsconfig = JSON.parse(cleaned);
160
- if (debug) {
161
- console.log("[next-sitemap] Parsed tsconfig paths:", tsconfig.compilerOptions?.paths);
162
- }
163
- const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
164
- const paths = tsconfig.compilerOptions?.paths || {};
165
- for (const [key, values] of Object.entries(paths)) {
166
- if (values.length > 0) {
167
- const aliasKey = key.replace(/\*$/, "");
168
- const aliasValue = joinPath(projectRoot, baseUrl, values[0].replace(/\*$/, ""));
169
- alias[aliasKey] = aliasValue;
170
- }
171
- }
172
- }
173
- } catch (error) {
174
- if (debug) {
175
- console.error("[next-sitemap] Error parsing tsconfig:", error);
176
- }
177
- }
178
- return alias;
179
- }
180
- function getJiti(projectRoot, debug = false) {
181
- if (jitiCache.has(projectRoot)) {
182
- return jitiCache.get(projectRoot);
183
- }
184
- const alias = getTsconfigPaths(projectRoot, debug);
185
- if (debug) {
186
- console.log("[next-sitemap] Final alias config:", JSON.stringify(alias));
187
- }
188
- const jiti = createJiti(import.meta.url, {
189
- moduleCache: false,
190
- interopDefault: true,
191
- jsx: true,
192
- alias
193
- });
194
- jitiCache.set(projectRoot, jiti);
195
- return jiti;
196
- }
197
- async function importPage(appDirectory, key, debug = false) {
198
- const relativePath = key.replace("./", "");
199
- const absolutePath = path.join(appDirectory, relativePath);
200
- const projectRoot = process.cwd();
201
- const jiti = getJiti(projectRoot, debug);
202
- const module = await jiti.import(absolutePath);
203
- return module.default || module;
292
+ const srcApp = join(process.cwd(), "src/app");
293
+ return existsSync(srcApp) ? srcApp : join(process.cwd(), "app");
204
294
  }
205
295
  function extractRoutes(pageKeys, localeSegment) {
206
296
  const routes = [];
@@ -208,163 +298,64 @@ function extractRoutes(pageKeys, localeSegment) {
208
298
  if (key.includes("[...")) continue;
209
299
  let pathname = key.replace("./", "/").replace(/\/page\.(tsx?|jsx?)$/, "");
210
300
  if (localeSegment) {
211
- pathname = pathname.replace(
212
- new RegExp(`^/${localeSegment.replace(/[[\]]/g, "\\$&")}`),
213
- ""
214
- );
301
+ const escapedSegment = localeSegment.replace(/[[\]]/g, "\\$&");
302
+ pathname = pathname.replace(new RegExp(`^/${escapedSegment}`), "");
215
303
  }
216
304
  pathname = pathname.replace(/\/\([^)]+\)/g, "");
217
305
  if (/(?:^|\/)(src|app)(?:\/|$)/.test(pathname)) continue;
218
- if (!pathname || pathname === "") {
219
- pathname = "/";
220
- } else if (!pathname.startsWith("/")) {
306
+ pathname = pathname || "/";
307
+ if (pathname !== "/" && !pathname.startsWith("/")) {
221
308
  pathname = "/" + pathname;
222
309
  }
223
- const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) || [];
224
- routes.push({
225
- pathname,
226
- dynamicSegments,
227
- key
228
- });
310
+ const dynamicSegments = pathname.match(/\[([^\]]+)\]/g)?.map((s) => s.slice(1, -1)) ?? [];
311
+ routes.push({ pathname, dynamicSegments, fileKey: key });
229
312
  }
230
313
  return routes;
231
314
  }
232
- async function getAllPaths(routes, appDirectory, debug = false) {
233
- const allPaths = ["/"];
234
- const seenPaths = /* @__PURE__ */ new Set(["/"]);
235
- for (const route of routes) {
236
- if (route.dynamicSegments.length === 0) {
237
- if (route.pathname !== "/" && !seenPaths.has(route.pathname)) {
238
- allPaths.push(route.pathname);
239
- seenPaths.add(route.pathname);
240
- }
241
- } else {
242
- let getParams = null;
243
- try {
244
- if (debug) {
245
- console.log(`[next-sitemap] ${route.pathname}: importing ${route.key}`);
246
- }
247
- const module = await importPage(appDirectory, route.key, debug);
248
- getParams = module.generateStaticParams || null;
249
- } catch (error) {
250
- if (debug) {
251
- console.warn(`[next-sitemap] ${route.pathname}: import failed:`, error);
252
- }
253
- }
254
- if (getParams) {
255
- try {
256
- const params = await getParams();
257
- if (debug) {
258
- console.log(`[next-sitemap] ${route.pathname}: generateStaticParams returned ${params.length} params`);
259
- }
260
- for (const param of params) {
261
- let dynamicPath = route.pathname;
262
- for (const segment of route.dynamicSegments) {
263
- const value = param[segment];
264
- if (value === void 0) {
265
- if (debug) {
266
- console.warn(`[next-sitemap] ${route.pathname}: missing param "${segment}" in`, param);
267
- }
268
- continue;
269
- }
270
- dynamicPath = dynamicPath.replace(`[${segment}]`, value);
271
- }
272
- if (!seenPaths.has(dynamicPath)) {
273
- allPaths.push(dynamicPath);
274
- seenPaths.add(dynamicPath);
275
- }
276
- }
277
- } catch (error) {
278
- console.error(`[next-sitemap] Error calling generateStaticParams for ${route.pathname}:`, error);
279
- }
280
- } else {
281
- if (debug) {
282
- console.warn(
283
- `[next-sitemap] Skipping dynamic route ${route.pathname}: no generateStaticParams exported. Use additionalSitemaps for routes that fetch data at runtime.`
284
- );
285
- }
286
- }
287
- }
288
- }
289
- return allPaths;
290
- }
291
- function pathsToEntries(paths, config) {
292
- const { baseUrl, locales = [], defaultLocale, exclude, priority, changeFreq } = config;
293
- const filteredPaths = paths.filter((pathname) => !shouldExclude(pathname, exclude));
294
- return filteredPaths.map((pathname) => {
295
- const entry = {
296
- url: buildUrl(baseUrl, pathname, defaultLocale, defaultLocale),
297
- lastModified: /* @__PURE__ */ new Date(),
298
- changeFrequency: getChangeFreq(pathname, changeFreq),
299
- priority: getPriority(pathname, priority)
300
- };
301
- if (locales.length > 0) {
302
- entry.alternates = {
303
- languages: Object.fromEntries(
304
- locales.map((locale) => [
305
- locale,
306
- buildUrl(baseUrl, pathname, locale, defaultLocale)
307
- ])
308
- )
309
- };
310
- }
311
- return entry;
312
- });
313
- }
314
315
  function createSitemapIndexHandler(options) {
315
316
  const { urlsPerSitemap = 5e3, locales = [], defaultLocale, additionalSitemaps, exclude, debug = false } = options;
316
317
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
317
- const appDir = resolveAppDirectory(options);
318
- const pageKeys = getPageKeys(options);
318
+ const appDir = resolveAppDirectory(options.appDirectory);
319
+ const pageKeys = findPageFiles(appDir);
319
320
  const routes = extractRoutes(pageKeys, localeSegment);
320
321
  return {
321
322
  GET: async () => {
322
323
  if (debug) {
323
- console.log(`[next-sitemap] Found ${routes.length} routes:`);
324
- routes.forEach((r) => {
325
- const isDynamic = r.dynamicSegments.length > 0;
326
- const segments = isDynamic ? ` [${r.dynamicSegments.join(", ")}]` : "";
327
- console.log(` ${r.pathname}${segments}${isDynamic ? " (dynamic)" : ""}`);
328
- });
324
+ console.log(`[next-sitemap] Found ${routes.length} routes`);
329
325
  }
330
- const allPaths = await getAllPaths(routes, appDir, debug);
331
- const filteredPaths = allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
326
+ const allPaths = await generateAllPaths(routes, appDir, debug);
327
+ const filteredPaths = allPaths.filter((p) => !shouldExclude(p, exclude));
332
328
  const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
333
- const xml = generateSitemapIndexXml(options.baseUrl, sitemapCount, {
334
- additionalSitemaps
335
- });
336
- return new Response(xml, {
337
- headers: { "Content-Type": "application/xml" }
338
- });
329
+ return new Response(
330
+ generateSitemapIndexXml(options.baseUrl, sitemapCount, { additionalSitemaps, poweredBy: options.poweredBy }),
331
+ { headers: { "Content-Type": "application/xml" } }
332
+ );
339
333
  }
340
334
  };
341
335
  }
342
336
  function createSitemapHandler(options) {
343
337
  const { urlsPerSitemap = 5e3, locales = [], defaultLocale, exclude, debug = false } = options;
344
338
  const localeSegment = options.localeSegment ?? (locales.length > 0 || defaultLocale ? "[locale]" : "");
345
- const appDir = resolveAppDirectory(options);
346
- const pageKeys = getPageKeys(options);
339
+ const appDir = resolveAppDirectory(options.appDirectory);
340
+ const pageKeys = findPageFiles(appDir);
347
341
  const routes = extractRoutes(pageKeys, localeSegment);
348
342
  const getFilteredPaths = async () => {
349
- const allPaths = await getAllPaths(routes, appDir, debug);
350
- return allPaths.filter((pathname) => !shouldExclude(pathname, exclude));
343
+ const allPaths = await generateAllPaths(routes, appDir, debug);
344
+ return allPaths.filter((p) => !shouldExclude(p, exclude));
351
345
  };
352
346
  return {
353
347
  generateStaticParams: async () => {
354
348
  const filteredPaths = await getFilteredPaths();
355
- const sitemapCount = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
356
- return Array.from({ length: sitemapCount }, (_, i) => ({ id: String(i) }));
349
+ const count = Math.max(1, Math.ceil(filteredPaths.length / urlsPerSitemap));
350
+ return Array.from({ length: count }, (_, i) => ({ id: String(i) }));
357
351
  },
358
352
  GET: async (_request, { params }) => {
359
353
  const { id } = await params;
360
354
  const sitemapId = parseInt(id, 10);
361
355
  const filteredPaths = await getFilteredPaths();
362
- const start = sitemapId * urlsPerSitemap;
363
- const end = start + urlsPerSitemap;
364
- const paths = filteredPaths.slice(start, end);
356
+ const paths = filteredPaths.slice(sitemapId * urlsPerSitemap, (sitemapId + 1) * urlsPerSitemap);
365
357
  const entries = pathsToEntries(paths, { ...options, exclude: void 0 });
366
- const xml = generateSitemapXml(entries);
367
- return new Response(xml, {
358
+ return new Response(generateSitemapXml(entries, { poweredBy: options.poweredBy }), {
368
359
  headers: { "Content-Type": "application/xml" }
369
360
  });
370
361
  }