polen
Version:
A framework for delightful GraphQL developer portals
209 lines (208 loc) • 9.02 kB
JavaScript
import { Content } from '#api/content/$';
import { polenVirtual } from '#api/vite/vi';
import { reportDiagnostics } from '#lib/file-router/diagnostic-reporter';
import { FileRouter } from '#lib/file-router/index';
import { debugPolen } from '#singletons/debug';
import { superjson } from '#singletons/superjson';
import mdx from '@mdx-js/rollup';
import { Arr, Cache, Path, Str } from '@wollybeard/kit';
import { recmaCodeHike, remarkCodeHike } from 'codehike/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
import { viProjectData } from './core.js';
const debug = debugPolen.sub(`vite-pages`);
export const viProjectRoutes = polenVirtual([`project`, `routes.jsx`], { allowPluginProcessing: true });
export const viProjectPagesCatalog = polenVirtual([`project`, `data`, `pages-catalog.jsonsuper`], {
allowPluginProcessing: true,
});
/**
* Pages plugin with tree support
*/
export const Pages = ({ config, }) => {
const scanPages = Cache.memoize(debug.trace(async function scanPages() {
const result = await Content.scan({
dir: config.paths.project.absolute.pages,
glob: `**/*.{md,mdx}`,
});
return result;
}));
const invalidateVirtualModules = (server) => {
const routesModule = server.moduleGraph.getModuleById(viProjectRoutes.id);
if (routesModule) {
server.moduleGraph.invalidateModule(routesModule);
debug(`Invalidated routes virtual module`);
}
const catalogModule = server.moduleGraph.getModuleById(viProjectPagesCatalog.id);
if (catalogModule) {
server.moduleGraph.invalidateModule(catalogModule);
debug(`Invalidated pages catalog virtual module`);
}
const projectDataModule = server.moduleGraph.getModuleById(viProjectData.id);
if (projectDataModule) {
server.moduleGraph.invalidateModule(projectDataModule);
debug(`Invalidated project data virtual module`);
}
};
const generateRoutesModule = (pages) => {
const $ = {
routes: `routes`,
};
const s = Str.Builder();
s `export const ${$.routes} = []`;
// Generate imports and route objects
for (const { route, metadata } of pages) {
const filePathExp = Path.format(route.file.path.absolute);
const pathExp = FileRouter.routeToPathExpression(route);
const $$ = {
...$,
Component: Str.Case.camel(`page ` + Str.titlizeSlug(pathExp)),
};
s `
import ${$$.Component} from '${filePathExp}'
${$$.routes}.push({
path: '${pathExp}',
Component: ${$$.Component},
metadata: ${JSON.stringify(metadata)}
})
`;
}
return s.render();
};
const chConfig = {
components: { code: 'CodeBlock' },
syntaxHighlighting: {
theme: `github-light`,
},
};
return [
// Plugin 1: MDX Processing
{
enforce: `pre`,
...mdx({
jsxImportSource: `polen/react`,
providerImportSource: `polen/mdx`,
remarkPlugins: [
// Parse frontmatter blocks so they're removed from content
remarkFrontmatter,
remarkGfm,
[remarkCodeHike, chConfig],
],
recmaPlugins: [
[recmaCodeHike, chConfig],
],
rehypePlugins: [],
}),
},
// Plugin 2: Pages Management
{
name: `polen:pages`,
// Dev server configuration
configureServer(server) {
// Add pages directory to watcher
debug(`configureServer: watch pages directory`, config.paths.project.absolute.pages);
server.watcher.add(config.paths.project.absolute.pages);
// Handle file additions and deletions
const handleFileStructureChange = async (file, event) => {
if (!Content.isPageFile(file, config.paths.project.absolute.pages))
return;
debug(`Page file ${event === `add` ? `added` : `deleted`}:`, file);
// Clear cache and rescan
scanPages.clear();
const newScanResult = await scanPages();
// Invalidate virtual modules
invalidateVirtualModules(server);
// Report any diagnostics
reportDiagnostics(newScanResult.diagnostics);
// Trigger full reload to ensure routes are updated
server.ws.send({ type: `full-reload` });
};
server.watcher.on(`add`, (file) => handleFileStructureChange(file, `add`));
server.watcher.on(`unlink`, (file) => handleFileStructureChange(file, `unlink`));
},
// Hot update handling for existing files
async handleHotUpdate({ file, server, modules }) {
debug(`handleHotUpdate`, file);
if (!Content.isPageFile(file, config.paths.project.absolute.pages))
return;
debug(`Page file changed:`, file);
// Get current pages before clearing cache
const oldPages = await scanPages();
// Clear cache and rescan
scanPages.clear();
const newScanResult = await scanPages();
// Check if the visible pages list changed. This can happen when:
// - A page's frontmatter `hidden` field changes (true <-> false)
// - A page's frontmatter affects its route (though we don't support this yet)
// If only the content changed (not frontmatter), we can use fast HMR.
const pageStructureChanged = !oldPages || !Arr.equalShallowly(oldPages.list.map(p => Path.format(p.route.file.path.absolute)), newScanResult.list.map(p => Path.format(p.route.file.path.absolute)));
if (!pageStructureChanged) {
debug(`Page content changed, allowing HMR`);
// Let default HMR handle the MDX file change
return modules;
}
//
// ━━ Manual Invalidation
//
debug(`Page structure changed, triggering full reload`);
// Invalidate virtual modules and trigger reload
invalidateVirtualModules(server);
reportDiagnostics(newScanResult.diagnostics);
server.ws.send({ type: `full-reload` });
return [];
},
resolveId(id) {
if (id === viProjectPagesCatalog.id) {
return viProjectPagesCatalog.resolved;
}
},
load: {
async handler(id) {
if (id !== viProjectPagesCatalog.resolved)
return;
debug(`hook load`);
const scanResult = await scanPages();
reportDiagnostics(scanResult.diagnostics);
debug(`found visible`, { count: scanResult.list.length });
//
// ━━ Build Sidebar
//
const sidebarIndex = Content.buildSidebarIndex(scanResult);
//
// ━━ Put It All together
//
const projectPagesCatalog = {
sidebarIndex,
pages: scanResult.list,
};
// Return just the JSON string - let the JSON plugin handle the transformation
return superjson.stringify(projectPagesCatalog);
},
},
},
// Plugin 3: Virtual Module for React Router Routes
{
name: `polen:routes`,
resolveId(id) {
if (id === viProjectRoutes.id) {
return viProjectRoutes.resolved;
}
},
load: {
handler: async (id) => {
if (id !== viProjectRoutes.resolved)
return;
debug(`Loading viProjectRoutes virtual module`);
const scanResult = await scanPages();
reportDiagnostics(scanResult.diagnostics);
const code = generateRoutesModule(scanResult.list);
// Generate the module code
return {
code,
moduleType: `js`,
};
},
},
},
];
};
//# sourceMappingURL=pages.js.map