@analogjs/platform
Version:
The fullstack meta-framework for Angular
170 lines • 14.5 kB
JavaScript
import { normalizePath } from 'vite';
import { globSync } from 'tinyglobby';
import { resolve } from 'node:path';
/**
* Router plugin that handles route file discovery and hot module replacement.
*
* This plugin provides three main functionalities:
* 1. Route invalidation when files are added/deleted (HMR workaround)
* 2. Dynamic route file discovery and import generation
* 3. Content route file discovery for markdown and Analog content
*
* @param options Configuration options for the router plugin
* @returns Array of Vite plugins for route handling
*
* IMPORTANT: This plugin uses tinyglobby for file discovery.
* Key behavior with { dot: true, absolute: true }:
* - Returns absolute paths for ALL discovered files
* - Path normalization is required to match expected output format
* - Files within project root must use relative paths in object keys
* - Files outside project root keep absolute paths in object keys
*/
export function routerPlugin(options) {
const workspaceRoot = normalizePath(options?.workspaceRoot ?? process.cwd());
let config;
let root;
const isRouteLikeFile = (path) => path.includes(`routes`) ||
path.includes(`pages`) ||
path.includes('content');
const invalidateFileModules = (server, path) => {
const normalizedPath = normalizePath(path);
// A newly added page can be discovered before its final contents settle.
// Invalidate the page module itself so later edits don't keep serving the
// first incomplete transform from Vite's module graph.
const fileModules = server.moduleGraph.getModulesByFile?.(normalizedPath) ??
server.moduleGraph.fileToModulesMap.get(normalizedPath);
fileModules?.forEach((mod) => {
server.moduleGraph.invalidateModule(mod);
mod.importers.forEach((imp) => {
server.moduleGraph.invalidateModule(imp);
});
});
};
return [
{
name: 'analogjs-router-invalidate-routes',
configureServer(server) {
/**
* Invalidates route modules when files are added or deleted.
* This is a workaround for Vite's HMR limitations with dynamic imports.
*
* @param path The file path that was added or deleted
*/
function invalidateRoutes(path) {
if (!isRouteLikeFile(path)) {
return;
}
invalidateFileModules(server, path);
server.moduleGraph.fileToModulesMap.forEach((mods) => {
mods.forEach((mod) => {
if (mod.id?.includes('analogjs') && mod.id?.includes('fesm')) {
server.moduleGraph.invalidateModule(mod);
mod.importers.forEach((imp) => {
server.moduleGraph.invalidateModule(imp);
});
}
});
});
server.ws.send({
type: 'full-reload',
});
}
server.watcher.on('add', invalidateRoutes);
server.watcher.on('change', invalidateRoutes);
server.watcher.on('unlink', invalidateRoutes);
},
},
{
name: 'analog-glob-routes',
config(_config) {
config = _config;
root = normalizePath(resolve(workspaceRoot, config.root || '.') || '.');
},
/**
* Transforms code to replace ANALOG_ROUTE_FILES and ANALOG_CONTENT_ROUTE_FILES
* placeholders with actual dynamic imports of discovered route and content files.
*
* @param code The source code to transform
* @returns Transformed code with dynamic imports or undefined if no transformation needed
*/
transform(code) {
if (code.includes('ANALOG_ROUTE_FILES') ||
code.includes('ANALOG_CONTENT_ROUTE_FILES')) {
// Discover route files using tinyglobby
// NOTE: { absolute: true } returns absolute paths for ALL files
const routeFiles = globSync([
`${root}/app/routes/**/*.ts`,
`${root}/src/app/routes/**/*.ts`,
`${root}/src/app/pages/**/*.page.ts`,
...(options?.additionalPagesDirs || [])?.map((glob) => `${workspaceRoot}${glob}/**/*.page.ts`),
], { dot: true, absolute: true });
// Discover content files using tinyglobby
const contentRouteFiles = globSync([
`${root}/src/app/routes/**/*.md`,
`${root}/src/app/pages/**/*.md`,
`${root}/src/content/**/*.md`,
...(options?.additionalContentDirs || [])?.map((glob) => `${workspaceRoot}${glob}/**/*.md`),
], { dot: true, absolute: true });
let result = code.replace('ANALOG_ROUTE_FILES = {};', `
ANALOG_ROUTE_FILES = {${routeFiles.map((module) => {
// CRITICAL: tinyglobby returns absolute paths, but we need relative paths for project files
// to match expected output format. Library files keep absolute paths.
const key = module.startsWith(root)
? module.replace(root, '')
: module;
return `"${key}": () => import('${module}')`;
})}};
`);
result = result.replace('ANALOG_CONTENT_ROUTE_FILES = {};', `
ANALOG_CONTENT_ROUTE_FILES = {${contentRouteFiles.map((module) => {
// Same path normalization as route files
const key = module.startsWith(root)
? module.replace(root, '')
: module;
return `"${key}": () => import('${module}?analog-content-file=true').then(m => m.default)`;
})}};
`);
return {
code: result,
map: { mappings: '' },
};
}
return;
},
},
{
name: 'analog-glob-endpoints',
/**
* Transforms code to replace ANALOG_PAGE_ENDPOINTS placeholder
* with actual dynamic imports of discovered server endpoint files.
*
* @param code The source code to transform
* @returns Transformed code with dynamic imports or undefined if no transformation needed
*/
transform(code) {
if (code.includes('ANALOG_PAGE_ENDPOINTS')) {
// Discover server endpoint files using tinyglobby
const endpointFiles = globSync([
`${root}/src/app/pages/**/*.server.ts`,
...(options?.additionalPagesDirs || []).map((glob) => `${workspaceRoot}${glob}/**/*.server.ts`),
], { dot: true, absolute: true });
const result = code.replace('ANALOG_PAGE_ENDPOINTS = {};', `
ANALOG_PAGE_ENDPOINTS = {${endpointFiles.map((module) => {
// Same path normalization for consistency
const key = module.startsWith(root)
? module.replace(root, '')
: module;
return `"${key}": () => import('${module}')`;
})}};
`);
return {
code: result,
map: { mappings: '' },
};
}
return;
},
},
];
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGVyLXBsdWdpbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL3BsYXRmb3JtL3NyYy9saWIvcm91dGVyLXBsdWdpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsYUFBYSxFQUFxQyxNQUFNLE1BQU0sQ0FBQztBQUN4RSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFJcEM7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsTUFBTSxVQUFVLFlBQVksQ0FBQyxPQUFpQjtJQUM1QyxNQUFNLGFBQWEsR0FBRyxhQUFhLENBQUMsT0FBTyxFQUFFLGFBQWEsSUFBSSxPQUFPLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUM3RSxJQUFJLE1BQWtCLENBQUM7SUFDdkIsSUFBSSxJQUFZLENBQUM7SUFDakIsTUFBTSxlQUFlLEdBQUcsQ0FBQyxJQUFZLEVBQUUsRUFBRSxDQUN2QyxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQztRQUN2QixJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQztRQUN0QixJQUFJLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzNCLE1BQU0scUJBQXFCLEdBQUcsQ0FBQyxNQUFxQixFQUFFLElBQVksRUFBRSxFQUFFO1FBQ3BFLE1BQU0sY0FBYyxHQUFHLGFBQWEsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMzQyx5RUFBeUU7UUFDekUsMEVBQTBFO1FBQzFFLHVEQUF1RDtRQUN2RCxNQUFNLFdBQVcsR0FDZixNQUFNLENBQUMsV0FBVyxDQUFDLGdCQUFnQixFQUFFLENBQUMsY0FBYyxDQUFDO1lBQ3JELE1BQU0sQ0FBQyxXQUFXLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBRTFELFdBQVcsRUFBRSxPQUFP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