@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 +1 -1
- package/src/engine/app/_preview/[sessionId]/[...slug]/page.tsx +0 -1
- package/src/engine/app/_preview/api/sessions/[sessionId]/init/route.ts +5 -1
- package/src/engine/app/_preview/api/sessions/[sessionId]/sync/route.ts +5 -1
- package/src/engine/lib/preview-content.ts +2 -0
- package/src/engine/lib/preview-source.ts +59 -4
package/package.json
CHANGED
|
@@ -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]
|
|
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]
|
|
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)
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
/**
|