@aravindc26/velu 0.13.4 → 0.13.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravindc26/velu",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "A modern documentation site generator powered by Markdown and JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -154,7 +154,6 @@ export default async function PreviewPage({ params }: PageProps) {
154
154
  let pageToc: any;
155
155
 
156
156
  if (typeof loadFn === 'function') {
157
- // Dynamic/async entry — compile MDX on demand
158
157
  const loaded = await loadFn();
159
158
  MDX = loaded.body;
160
159
  pageToc = loaded.toc;
@@ -11,12 +11,16 @@ export async function POST(
11
11
 
12
12
  const { sessionId } = await params;
13
13
 
14
+ console.log(`[PREVIEW:init] START session=${sessionId}`);
15
+
14
16
  try {
15
17
  const result = generateSessionContent(sessionId);
18
+ console.log(`[PREVIEW:init] generateSessionContent result:`, JSON.stringify(result));
16
19
 
17
20
  // Invalidate the cached dynamic source so the next page request
18
21
  // re-scans the content directory and picks up the new files.
19
22
  invalidateSessionSource(sessionId);
23
+ console.log(`[PREVIEW:init] DONE session=${sessionId}`);
20
24
 
21
25
  return Response.json({
22
26
  status: 'ready',
@@ -26,7 +30,7 @@ export async function POST(
26
30
  });
27
31
  } catch (error) {
28
32
  const message = error instanceof Error ? error.message : 'Unknown error';
29
- console.error(`[PREVIEW] Init failed for session ${sessionId}:`, message);
33
+ console.error(`[PREVIEW:init] FAILED session=${sessionId}:`, message);
30
34
  return Response.json(
31
35
  { status: 'error', error: message },
32
36
  { status: 500 },
@@ -12,6 +12,8 @@ export async function POST(
12
12
  const { sessionId } = await params;
13
13
  const file = request.nextUrl.searchParams.get('file');
14
14
 
15
+ console.log(`[PREVIEW:sync] START session=${sessionId} file=${file}`);
16
+
15
17
  if (!file) {
16
18
  return Response.json(
17
19
  { error: 'Missing "file" query parameter' },
@@ -21,9 +23,11 @@ export async function POST(
21
23
 
22
24
  try {
23
25
  const result = syncSessionFile(sessionId, file);
26
+ console.log(`[PREVIEW:sync] syncSessionFile result:`, JSON.stringify(result));
24
27
 
25
28
  // Invalidate cached source so next page request re-scans content
26
29
  invalidateSessionSource(sessionId);
30
+ console.log(`[PREVIEW:sync] DONE session=${sessionId} file=${file}`);
27
31
 
28
32
  return Response.json({
29
33
  status: 'synced',
@@ -32,7 +36,7 @@ export async function POST(
32
36
  });
33
37
  } catch (error) {
34
38
  const message = error instanceof Error ? error.message : 'Unknown error';
35
- console.error(`[PREVIEW] Sync failed for session ${sessionId}, file ${file}:`, message);
39
+ console.error(`[PREVIEW:sync] FAILED session=${sessionId} file=${file}:`, message);
36
40
  return Response.json(
37
41
  { status: 'error', error: message },
38
42
  { status: 500 },
@@ -755,6 +755,8 @@ export function syncSessionFile(
755
755
  const workspaceDir = join(WORKSPACE_DIR, sessionId);
756
756
  const outputDir = join(PREVIEW_CONTENT_DIR, sessionId);
757
757
 
758
+ console.log(`[PREVIEW:syncFile] session=${sessionId} file=${filePath}`);
759
+
758
760
  if (filePath === PRIMARY_CONFIG_NAME || filePath === LEGACY_CONFIG_NAME) {
759
761
  generateSessionContent(sessionId);
760
762
  return { synced: true };
@@ -9,7 +9,7 @@
9
9
  * Used only in preview mode (PREVIEW_MODE=true) — production builds still use
10
10
  * the standard `source.ts` with build-time collections.
11
11
  */
12
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
13
13
  import { join, relative } from 'node:path';
14
14
  import { loader } from 'fumadocs-core/source';
15
15
  import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
@@ -24,8 +24,37 @@ import * as sourceConfigExports from '../source.config';
24
24
 
25
25
  const PREVIEW_CONTENT_DIR = process.env.PREVIEW_CONTENT_DIR || './content';
26
26
 
27
+ function log(tag: string, msg: string, data?: Record<string, unknown>) {
28
+ const payload = data ? ' ' + JSON.stringify(data) : '';
29
+ console.log(`[PREVIEW:${tag}] ${msg}${payload}`);
30
+ }
31
+
27
32
  // ── Cache ──────────────────────────────────────────────────────────────────
28
33
 
34
+ // File-based invalidation signal. Next.js bundles API routes and page routes
35
+ // into separate module instances, so in-memory invalidation from the sync/init
36
+ // route handler does NOT clear the cache seen by the page renderer. Instead,
37
+ // the invalidation writes a timestamp to a file on disk which both sides can see.
38
+ const INVALIDATION_DIR = join(PREVIEW_CONTENT_DIR, '.invalidation');
39
+
40
+ function getInvalidationPath(sessionId: string): string {
41
+ return join(INVALIDATION_DIR, `${sessionId}.stamp`);
42
+ }
43
+
44
+ function readInvalidationStamp(sessionId: string): number {
45
+ const p = getInvalidationPath(sessionId);
46
+ try {
47
+ return Number(readFileSync(p, 'utf-8').trim()) || 0;
48
+ } catch {
49
+ return 0;
50
+ }
51
+ }
52
+
53
+ function writeInvalidationStamp(sessionId: string): void {
54
+ mkdirSync(INVALIDATION_DIR, { recursive: true });
55
+ writeFileSync(getInvalidationPath(sessionId), String(Date.now()), 'utf-8');
56
+ }
57
+
29
58
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
59
  const sourceCache = new Map<string, { source: any; createdAt: number }>();
31
60
 
@@ -37,6 +66,7 @@ async function getDynamic() {
37
66
  if (dynamicInstance) return dynamicInstance;
38
67
  if (dynamicInitPromise) return dynamicInitPromise;
39
68
 
69
+ log('dynamic', 'Initializing new dynamic() instance');
40
70
  dynamicInitPromise = dynamic(
41
71
  sourceConfigExports,
42
72
  {
@@ -47,6 +77,7 @@ async function getDynamic() {
47
77
  ).then((inst) => {
48
78
  dynamicInstance = inst;
49
79
  dynamicInitPromise = null;
80
+ log('dynamic', 'dynamic() instance ready');
50
81
  return inst;
51
82
  });
52
83
 
@@ -146,11 +177,22 @@ function scanContentDir(sessionDir: string): {
146
177
  */
147
178
  export async function getSessionSource(sessionId: string) {
148
179
  const cached = sourceCache.get(sessionId);
149
- if (cached) return cached.source;
180
+ if (cached) {
181
+ const stamp = readInvalidationStamp(sessionId);
182
+ if (stamp <= cached.createdAt) {
183
+ const age = Date.now() - cached.createdAt;
184
+ log('source', `Cache HIT for session ${sessionId}`, { ageMs: age });
185
+ return cached.source;
186
+ }
187
+ log('source', `Cache STALE for session ${sessionId} (stamp=${stamp} > created=${cached.createdAt}), rebuilding`);
188
+ sourceCache.delete(sessionId);
189
+ } else {
190
+ log('source', `Cache MISS for session ${sessionId}`);
191
+ }
150
192
 
151
193
  const sessionDir = join(PREVIEW_CONTENT_DIR, sessionId);
152
194
  if (!existsSync(sessionDir)) {
153
- // Return an empty source if no content yet
195
+ log('source', `Session dir does not exist: ${sessionDir}`);
154
196
  const emptySource = loader({
155
197
  baseUrl: '/',
156
198
  source: { files: [] },
@@ -161,6 +203,11 @@ export async function getSessionSource(sessionId: string) {
161
203
  const dyn = await getDynamic();
162
204
  const { entries, metaFiles } = scanContentDir(sessionDir);
163
205
 
206
+ log('source', `Scanned ${sessionDir}`, {
207
+ entryCount: entries.length,
208
+ metaFileCount: Object.keys(metaFiles).length,
209
+ });
210
+
164
211
  const collection = await dyn.docs('docs', sessionDir, metaFiles, entries);
165
212
  const fumadocsSource = collection.toFumadocsSource();
166
213
 
@@ -188,15 +235,23 @@ export async function getSessionSource(sessionId: string) {
188
235
  });
189
236
 
190
237
  sourceCache.set(sessionId, { source: src, createdAt: Date.now() });
238
+ log('source', `Built and cached source for session ${sessionId}`, {
239
+ pageCount: src.getPages().length,
240
+ });
191
241
  return src;
192
242
  }
193
243
 
194
244
  /**
195
245
  * Invalidate the cached source for a session.
196
- * Call after content generation or sync so the next request re-scans.
246
+ * Writes a file-based timestamp so that ALL Next.js module instances
247
+ * (API routes AND page routes) see the invalidation, even though they
248
+ * run in separate bundles with separate in-memory Maps.
197
249
  */
198
250
  export function invalidateSessionSource(sessionId: string): void {
251
+ const had = sourceCache.has(sessionId);
199
252
  sourceCache.delete(sessionId);
253
+ writeInvalidationStamp(sessionId);
254
+ log('invalidate', `session=${sessionId} hadCache=${had} wroteStamp=true`);
200
255
  }
201
256
 
202
257
  /**