Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 17 additions & 121 deletions packages/react/src/plugin-pages.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { existsSync } from 'node:fs';
import { dirname, relative } from 'pathslash';
import { normalizePath, type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite';
import {
generateComponentManifest,
resolveStaticPageDataBuildOptions,
scanPages,
} from 'void/pages';
import type { Plugin } from 'vite';
import { createPagesPlugin, generateComponentManifest } from 'void/pages';
import type { PageScanResult, StaticPageDataBuildOptions } from 'void/pages';

const RENDERER_ID = 'virtual:void-pages-renderer';
const CLIENT_ENTRY_ID = 'virtual:void-pages-client';
const WORKER_ENVIRONMENT_NAME = 'void_worker';
const REACT_DEDUPE = ['react', 'react-dom'];
Expand Down Expand Up @@ -39,122 +33,24 @@ type PagesPluginOptions = {
};

export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plugin {
let pageScan: PageScanResult | null = null;
let isDev = false;
let viteConfig: ResolvedConfig | undefined;
const root = options?.root ?? dirname(pagesDir);
const normalizedPagesDir = normalizePath(pagesDir);
const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
let pendingRescan: Promise<void> | null = null;
const rescanPages = async () => {
pageScan = existsSync(pagesDir)
? await scanPages(root, { pagesDir, environment: isDev ? 'dev' : 'prod' })
: null;
};
const runRescan = () => {
pendingRescan ??= rescanPages().finally(() => {
pendingRescan = null;
});
return pendingRescan;
};

const invalidatePageModules = (server: ViteDevServer) => {
for (const env of Object.values(server.environments)) {
for (const id of ['\0' + RENDERER_ID, '\0' + CLIENT_ENTRY_ID]) {
const mod = env.moduleGraph.getModuleById(id);
if (mod) {
env.moduleGraph.invalidateModule(mod);
}
}
}
};

return {
return createPagesPlugin(pagesDir, {
name: 'void-react:pages',

async buildStart() {
await runRescan();
root,
generateRenderer(scan, isDev) {
return generateRendererModule(scan, pagesDir, isDev);
},

resolveId: {
filter: { id: /^virtual:void-pages-(renderer|client)$/ },
handler(id) {
return '\0' + id;
},
generateClient(scan, staticPageData) {
return generateClientEntry(
scan,
pagesDir,
options?.viewTransitions,
options?.prefetch,
staticPageData,
);
},

load: {
// eslint-disable-next-line no-control-regex -- Vite virtual module null-byte prefix
filter: { id: /^\0virtual:void-pages-(renderer|client)$/ },
async handler(id) {
if (isDev && pendingRescan) {
await pendingRescan;
}
if (!pageScan && existsSync(pagesDir)) {
await runRescan();
}
if (id === '\0' + RENDERER_ID) {
if (!pageScan) {
return "export function renderPage() { throw new Error('No pages found'); }";
}
return generateRendererModule(pageScan, pagesDir, isDev);
}
if (id === '\0' + CLIENT_ENTRY_ID) {
if (!pageScan) {
return '';
}
return generateClientEntry(
pageScan,
pagesDir,
options?.viewTransitions,
options?.prefetch,
resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }),
);
}
},
},

configureServer(server) {
const handlePagesChange = async (path: string, event: 'add' | 'change' | 'unlink') => {
const normalizedPath = normalizePath(path);
if (
normalizedPath !== normalizedPagesDir &&
!normalizedPath.startsWith(normalizedPagesDir + '/')
) {
return;
}
if (event === 'change' && !normalizedPath.endsWith('.md')) {
return;
}
try {
await runRescan();
} catch (error) {
// Keep the last good scan; the next change rescans.
server.config.logger.error(error instanceof Error ? error.message : String(error), {
error: error instanceof Error ? error : undefined,
timestamp: true,
});
return;
}
invalidatePageModules(server);
};

server.watcher.on('add', (path) => handlePagesChange(path, 'add'));
server.watcher.on('change', (path) => handlePagesChange(path, 'change'));
server.watcher.on('unlink', (path) => handlePagesChange(path, 'unlink'));
},

configResolved(config) {
viteConfig = config;
},

async config(_, env) {
// Set before the early return: pages/ may be created after the server starts.
isDev = env.command === 'serve';
if (!existsSync(pagesDir)) {
return;
}

async config({ isDev, scan }) {
// In dev mode, exclude @void/react from dep optimization so the
// virtual module and user components share the same React context objects.
// Without this, Vite pre-bundles @void/react, creating separate
Expand Down Expand Up @@ -193,7 +89,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
}

// Skip the pages-client bundle when all pages are island pages
await runRescan();
const pageScan = await scan();
if (!pageScan?.pages.some((page) => !page.island)) {
return;
}
Expand All @@ -210,7 +106,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
},
};
},
};
});
}

function generateRendererModule(scan: PageScanResult, pagesDir: string, isDev: boolean): string {
Expand Down
140 changes: 17 additions & 123 deletions packages/solid/src/plugin-pages.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { existsSync } from 'node:fs';
import { dirname } from 'pathslash';
import { normalizePath, type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite';
import {
generateComponentManifest,
resolveStaticPageDataBuildOptions,
scanPages,
} from 'void/pages';
import type { Plugin } from 'vite';
import { createPagesPlugin, generateComponentManifest } from 'void/pages';
import type { PageScanResult, StaticPageDataBuildOptions } from 'void/pages';

const RENDERER_ID = 'virtual:void-pages-renderer';
const CLIENT_ENTRY_ID = 'virtual:void-pages-client';

export type PrefetchConfig = {
Expand All @@ -23,121 +16,22 @@ type PagesPluginOptions = {
};

export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plugin {
let pageScan: PageScanResult | null = null;
let isDev = false;
let viteConfig: ResolvedConfig | undefined;
const root = options?.root ?? dirname(pagesDir);
const normalizedPagesDir = normalizePath(pagesDir);
let pendingRescan: Promise<void> | null = null;
const rescanPages = async () => {
pageScan = existsSync(pagesDir)
? await scanPages(root, { pagesDir, environment: isDev ? 'dev' : 'prod' })
: null;
};
const runRescan = () => {
pendingRescan ??= rescanPages().finally(() => {
pendingRescan = null;
});
return pendingRescan;
};

const invalidatePageModules = (server: ViteDevServer) => {
for (const env of Object.values(server.environments)) {
for (const id of ['\0' + RENDERER_ID, '\0' + CLIENT_ENTRY_ID]) {
const mod = env.moduleGraph.getModuleById(id);
if (mod) {
env.moduleGraph.invalidateModule(mod);
}
}
}
};

return {
return createPagesPlugin(pagesDir, {
name: 'void-solid:pages',

async buildStart() {
await runRescan();
root: options?.root,
generateRenderer(scan) {
return generateRendererModule(scan, pagesDir);
},

resolveId: {
filter: { id: /^virtual:void-pages-(renderer|client)$/ },
handler(id) {
return '\0' + id;
},
generateClient(scan, staticPageData) {
return generateClientEntry(
scan,
pagesDir,
options?.viewTransitions,
options?.prefetch,
staticPageData,
);
},

load: {
// eslint-disable-next-line no-control-regex -- Vite virtual module null-byte prefix
filter: { id: /^\0virtual:void-pages-(renderer|client)$/ },
async handler(id) {
if (isDev && pendingRescan) {
await pendingRescan;
}
if (!pageScan && existsSync(pagesDir)) {
await runRescan();
}
if (id === '\0' + RENDERER_ID) {
if (!pageScan) {
return "export function renderPage() { throw new Error('No pages found'); }";
}
return generateRendererModule(pageScan, pagesDir);
}
if (id === '\0' + CLIENT_ENTRY_ID) {
if (!pageScan) {
return '';
}
return generateClientEntry(
pageScan,
pagesDir,
options?.viewTransitions,
options?.prefetch,
resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }),
);
}
},
},

configureServer(server) {
const handlePagesChange = async (path: string, event: 'add' | 'change' | 'unlink') => {
const normalizedPath = normalizePath(path);
if (
normalizedPath !== normalizedPagesDir &&
!normalizedPath.startsWith(normalizedPagesDir + '/')
) {
return;
}
if (event === 'change' && !normalizedPath.endsWith('.md')) {
return;
}
try {
await runRescan();
} catch (error) {
// Keep the last good scan; the next change rescans.
server.config.logger.error(error instanceof Error ? error.message : String(error), {
error: error instanceof Error ? error : undefined,
timestamp: true,
});
return;
}
invalidatePageModules(server);
};

server.watcher.on('add', (path) => handlePagesChange(path, 'add'));
server.watcher.on('change', (path) => handlePagesChange(path, 'change'));
server.watcher.on('unlink', (path) => handlePagesChange(path, 'unlink'));
},

configResolved(config) {
viteConfig = config;
},

async config(_, env) {
// Set before the early return: pages/ may be created after the server starts.
isDev = env.command === 'serve';
if (!existsSync(pagesDir)) {
return;
}

async config({ isDev, scan }) {
if (isDev) {
return {
optimizeDeps: {
Expand All @@ -148,7 +42,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
}

// Skip the pages-client bundle when all pages are island pages
await runRescan();
const pageScan = await scan();
if (!pageScan?.pages.some((page) => !page.island)) {
return;
}
Expand All @@ -162,7 +56,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
},
};
},
};
});
}

function generateRendererModule(scan: PageScanResult, pagesDir: string): string {
Expand Down
Loading