UNPKG

@analogjs/vite-plugin-nitro

Version:

A Vite plugin for adding a nitro API server

479 lines 23.7 kB
import { build, createDevServer, createNitro } from 'nitropack'; import { toNodeListener } from 'h3'; import { mergeConfig, normalizePath } from 'vite'; import { dirname, relative, resolve } from 'node:path'; import { platform } from 'node:os'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { existsSync, readFileSync } from 'node:fs'; import { buildServer } from './build-server.js'; import { buildSSRApp } from './build-ssr.js'; import { pageEndpointsPlugin } from './plugins/page-endpoints.js'; import { getPageHandlers } from './utils/get-page-handlers.js'; import { buildSitemap } from './build-sitemap.js'; import { devServerPlugin } from './plugins/dev-server-plugin.js'; import { getMatchingContentFilesWithFrontMatter } from './utils/get-content-files.js'; import { ssrRenderer, clientRenderer, apiMiddleware, } from './utils/renderers.js'; import { expandRoutesWithLocales, createI18nPostRenderingHook, } from './utils/i18n-prerender.js'; const isWindows = platform() === 'win32'; const filePrefix = isWindows ? 'file:///' : ''; let clientOutputPath = ''; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export function nitro(options, nitroOptions) { const workspaceRoot = options?.workspaceRoot ?? process.cwd(); const sourceRoot = options?.sourceRoot ?? 'src'; let isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST']; const baseURL = process.env['NITRO_APP_BASE_URL'] || ''; const prefix = baseURL ? baseURL.substring(0, baseURL.length - 1) : ''; const apiPrefix = `/${options?.apiPrefix || 'api'}`; const useAPIMiddleware = typeof options?.useAPIMiddleware !== 'undefined' ? options?.useAPIMiddleware : true; let isBuild = false; let isServe = false; let ssrBuild = false; let config; let nitroConfig; let environmentBuild = false; let hasAPIDir = false; const routeSitemaps = {}; const routeSourceFiles = {}; let rootDir = workspaceRoot; return [ (options?.ssr ? devServerPlugin({ entryServer: options?.entryServer, index: options?.index, routeRules: nitroOptions?.routeRules, i18n: options?.i18n, }) : false), { name: '@analogjs/vite-plugin-nitro', async config(userConfig, { mode, command }) { isServe = command === 'serve'; isBuild = command === 'build'; ssrBuild = userConfig.build?.ssr === true; config = userConfig; isTest = isTest ? isTest : mode === 'test'; rootDir = relative(workspaceRoot, config.root || '.') || '.'; hasAPIDir = existsSync(resolve(workspaceRoot, rootDir, `${sourceRoot}/server/routes/${options?.apiPrefix || 'api'}`)); const buildPreset = process.env['BUILD_PRESET'] ?? nitroOptions?.preset; const pageHandlers = getPageHandlers({ workspaceRoot, sourceRoot, rootDir, additionalPagesDirs: options?.additionalPagesDirs, hasAPIDir, }); nitroConfig = { rootDir, preset: buildPreset, compatibilityDate: '2024-11-19', logLevel: nitroOptions?.logLevel || 0, srcDir: normalizePath(`${sourceRoot}/server`), scanDirs: [ normalizePath(`${rootDir}/${sourceRoot}/server`), ...(options?.additionalAPIDirs || []).map((dir) => normalizePath(`${workspaceRoot}${dir}`)), ], output: { dir: normalizePath(resolve(workspaceRoot, 'dist', rootDir, 'analog')), publicDir: normalizePath(resolve(workspaceRoot, 'dist', rootDir, 'analog/public')), }, buildDir: normalizePath(resolve(workspaceRoot, 'dist', rootDir, '.nitro')), typescript: { generateTsConfig: false, }, runtimeConfig: { apiPrefix: apiPrefix.substring(1), prefix, }, // Fixes support for Rolldown imports: { autoImport: false, }, rollupConfig: { onwarn(warning) { if (warning.message.includes('empty chunk') && warning.message.endsWith('.server')) { return; } }, plugins: [pageEndpointsPlugin()], }, handlers: [ ...(hasAPIDir ? [] : useAPIMiddleware ? [ { handler: '#ANALOG_API_MIDDLEWARE', middleware: true, }, ] : []), ...pageHandlers, ], routeRules: hasAPIDir ? undefined : useAPIMiddleware ? undefined : { [`${prefix}${apiPrefix}/**`]: { proxy: { to: '/**' }, }, }, virtual: { '#ANALOG_SSR_RENDERER': ssrRenderer, '#ANALOG_CLIENT_RENDERER': clientRenderer, ...(hasAPIDir ? {} : { '#ANALOG_API_MIDDLEWARE': apiMiddleware }), }, }; if (isVercelPreset(buildPreset)) { nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot); } if (isCloudflarePreset(buildPreset)) { nitroConfig = withCloudflareOutput(nitroConfig); } if (isNetlifyPreset(buildPreset) && rootDir === '.' && !existsSync(resolve(workspaceRoot, 'netlify.toml'))) { nitroConfig = withNetlifyOutputAPI(nitroConfig, workspaceRoot); } if (isFirebaseAppHosting()) { nitroConfig = withAppHostingOutput(nitroConfig); } if (!ssrBuild && !isTest) { // store the client output path for the SSR build config clientOutputPath = resolve(workspaceRoot, rootDir, config.build?.outDir || 'dist/client'); } const indexEntry = normalizePath(resolve(clientOutputPath, 'index.html')); nitroConfig.alias = { '#analog/index': indexEntry, }; if (isBuild) { nitroConfig.publicAssets = [{ dir: clientOutputPath }]; nitroConfig.renderer = options?.ssr ? '#ANALOG_SSR_RENDERER' : '#ANALOG_CLIENT_RENDERER'; if (isEmptyPrerenderRoutes(options)) { nitroConfig.prerender = {}; nitroConfig.prerender.routes = ['/']; } if (options?.prerender) { nitroConfig.prerender = nitroConfig.prerender ?? {}; nitroConfig.prerender.crawlLinks = options?.prerender?.discover; let routes = []; const prerenderRoutes = options?.prerender?.routes; if (isArrayWithElements(prerenderRoutes)) { routes = prerenderRoutes; } else if (typeof prerenderRoutes === 'function') { routes = await prerenderRoutes(); } nitroConfig.prerender.routes = routes.reduce((prev, current) => { if (!current) { return prev; } if (typeof current === 'string') { prev.push(current); return prev; } if ('route' in current) { if (current.sitemap) { routeSitemaps[current.route] = current.sitemap; } if (current.outputSourceFile) { const sourcePath = resolve(workspaceRoot, rootDir, current.outputSourceFile); routeSourceFiles[current.route] = readFileSync(sourcePath, 'utf8'); } prev.push(current.route); // Add the server-side data fetching endpoint URL if ('staticData' in current) { prev.push(`${apiPrefix}/_analog/pages/${current.route}`); } return prev; } const affectedFiles = getMatchingContentFilesWithFrontMatter(workspaceRoot, rootDir, current.contentDir, current.recursive); affectedFiles.forEach((f) => { const result = current.transform(f); if (result) { if (current.sitemap) { routeSitemaps[result] = current.sitemap && typeof current.sitemap === 'function' ? current.sitemap?.(f) : current.sitemap; } if (current.outputSourceFile) { const sourceContent = current.outputSourceFile(f); if (sourceContent) { routeSourceFiles[result] = sourceContent; } } prev.push(result); // Add the server-side data fetching endpoint URL if ('staticData' in current) { prev.push(`${apiPrefix}/_analog/pages/${result}`); } } }); return prev; }, []); // Expand routes with locale prefixes when i18n is configured if (options?.i18n && nitroConfig.prerender?.routes) { nitroConfig.prerender.routes = expandRoutesWithLocales(nitroConfig.prerender.routes.filter(Boolean), options.i18n); } } // Register i18n post-rendering hook for lang attribute injection if (options?.i18n) { options.prerender = options.prerender ?? {}; options.prerender.postRenderingHooks = options.prerender.postRenderingHooks ?? []; options.prerender.postRenderingHooks.push(createI18nPostRenderingHook(options.i18n)); } if (ssrBuild) { if (isWindows) { nitroConfig.externals = { inline: ['std-env'], }; } nitroConfig = { ...nitroConfig, externals: { ...nitroConfig.externals, external: ['rxjs', 'node-fetch-native/dist/polyfill'], }, moduleSideEffects: ['zone.js/node', 'zone.js/fesm2015/zone-node'], handlers: [ ...(hasAPIDir ? [] : useAPIMiddleware ? [ { handler: '#ANALOG_API_MIDDLEWARE', middleware: true, }, ] : []), ...pageHandlers, ], }; } } nitroConfig = mergeConfig(nitroConfig, nitroOptions); return { environments: { client: { build: { outDir: config?.build?.outDir || resolve(workspaceRoot, 'dist', rootDir, 'client'), }, }, ssr: { build: { ssr: true, rollupOptions: { input: options?.entryServer || resolve(workspaceRoot, rootDir, `${sourceRoot}/main.server.ts`), }, outDir: options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, 'ssr'), }, }, }, builder: { sharedPlugins: true, buildApp: async (builder) => { environmentBuild = true; const builds = [builder.build(builder.environments['client'])]; if (options?.ssr || nitroConfig.prerender?.routes?.length) { builds.push(builder.build(builder.environments['ssr'])); } await Promise.all(builds); let ssrEntryPath = resolve(options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, `ssr`), `main.server${filePrefix ? '.js' : ''}`); // add check for main.server.mjs fallback on Windows if (isWindows && !existsSync(ssrEntryPath)) { ssrEntryPath = ssrEntryPath.replace('.js', '.mjs'); } const ssrEntry = normalizePath(filePrefix + ssrEntryPath); nitroConfig.alias = { ...nitroConfig.alias, '#analog/ssr': ssrEntry, }; await buildServer(options, nitroConfig, routeSourceFiles); if (nitroConfig.prerender?.routes?.length && options?.prerender?.sitemap) { const publicDir = nitroConfig.output?.publicDir; if (!publicDir) { throw new Error('Nitro public output directory is required to build the sitemap.'); } console.log('Building Sitemap...'); // sitemap needs to be built after all directories are built await buildSitemap(config, options.prerender.sitemap, nitroConfig.prerender.routes, publicDir, routeSitemaps, options.i18n); } console.log(`\n\nThe '@analogjs/platform' server has been successfully built.`); }, }, }; }, async configureServer(viteServer) { if (isServe && !isTest) { const nitro = await createNitro({ dev: true, ...nitroConfig, }); const server = createDevServer(nitro); await build(nitro); const apiHandler = toNodeListener(server.app); if (hasAPIDir) { viteServer.middlewares.use((req, res, next) => { if (req.url?.startsWith(`${prefix}${apiPrefix}`)) { apiHandler(req, res); return; } next(); }); } else { viteServer.middlewares.use(apiPrefix, apiHandler); } viteServer.httpServer?.once('listening', () => { process.env['ANALOG_HOST'] = !viteServer.config.server.host ? 'localhost' : viteServer.config.server.host; process.env['ANALOG_PORT'] = `${viteServer.config.server.port}`; }); // handle upgrades if websockets are enabled if (nitroOptions?.experimental?.websocket) { viteServer.httpServer?.on('upgrade', server.upgrade); } console.log(`\n\nThe server endpoints are accessible under the "${prefix}${apiPrefix}" path.`); } }, async closeBundle() { // Skip when build is triggered by the Environment API if (environmentBuild) { return; } if (ssrBuild) { return; } if (isBuild) { if (options?.ssr) { console.log('Building SSR application...'); await buildSSRApp(config, options); } if (nitroConfig.prerender?.routes?.length && options?.prerender?.sitemap) { console.log('Building Sitemap...'); // sitemap needs to be built after all directories are built await buildSitemap(config, options.prerender.sitemap, nitroConfig.prerender.routes, clientOutputPath, routeSitemaps, options.i18n); } let ssrEntryPath = resolve(options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, `ssr`), `main.server${filePrefix ? '.js' : ''}`); // add check for main.server.mjs fallback on Windows if (isWindows && !existsSync(ssrEntryPath)) { ssrEntryPath = ssrEntryPath.replace('.js', '.mjs'); } const ssrEntry = normalizePath(filePrefix + ssrEntryPath); nitroConfig.alias = { ...nitroConfig.alias, '#analog/ssr': ssrEntry, }; await buildServer(options, nitroConfig, routeSourceFiles); console.log(`\n\nThe '@analogjs/platform' server has been successfully built.`); } }, }, { name: '@analogjs/vite-plugin-nitro-api-prefix', config() { return { define: { ANALOG_API_PREFIX: `"${baseURL.substring(1)}${apiPrefix.substring(1)}"`, ...(options?.i18n ? { ANALOG_I18N_DEFAULT_LOCALE: JSON.stringify(options.i18n.defaultLocale), ANALOG_I18N_LOCALES: JSON.stringify(options.i18n.locales), } : {}), }, }; }, }, ]; } function isEmptyPrerenderRoutes(options) { if (!options || isArrayWithElements(options?.prerender?.routes)) { return false; } return !options.prerender?.routes; } function isArrayWithElements(arr) { return !!(Array.isArray(arr) && arr.length); } const isVercelPreset = (buildPreset) => process.env['VERCEL'] || (buildPreset && buildPreset.toLowerCase().includes('vercel')); const withVercelOutputAPI = (nitroConfig, workspaceRoot) => ({ ...nitroConfig, output: { ...nitroConfig?.output, dir: normalizePath(resolve(workspaceRoot, '.vercel', 'output')), publicDir: normalizePath(resolve(workspaceRoot, '.vercel', 'output/static')), }, }); const isCloudflarePreset = (buildPreset) => process.env['CF_PAGES'] || (buildPreset && buildPreset.toLowerCase().includes('cloudflare-pages')); const withCloudflareOutput = (nitroConfig) => ({ ...nitroConfig, output: { ...nitroConfig?.output, serverDir: '{{ output.publicDir }}/_worker.js', }, }); const isFirebaseAppHosting = () => !!process.env['NG_BUILD_LOGS_JSON']; const withAppHostingOutput = (nitroConfig) => { let hasOutput = false; return { ...nitroConfig, serveStatic: true, rollupConfig: { ...nitroConfig.rollupConfig, output: { ...nitroConfig.rollupConfig?.output, entryFileNames: 'server.mjs', }, }, hooks: { ...nitroConfig.hooks, compiled: () => { if (!hasOutput) { const buildOutput = { errors: [], warnings: [], outputPaths: { root: pathToFileURL(`${nitroConfig.output?.dir}`), browser: pathToFileURL(`${nitroConfig.output?.publicDir}`), server: pathToFileURL(`${nitroConfig.output?.dir}/server`), }, }; // Log the build output for Firebase App Hosting to pick up console.log(JSON.stringify(buildOutput, null, 2)); hasOutput = true; } }, }, }; }; const isNetlifyPreset = (buildPreset) => process.env['NETLIFY'] || (buildPreset && buildPreset.toLowerCase().includes('netlify')); const withNetlifyOutputAPI = (nitroConfig, workspaceRoot) => ({ ...nitroConfig, output: { ...nitroConfig?.output, dir: normalizePath(resolve(workspaceRoot, 'netlify/functions')), }, }); //# sourceMappingURL=vite-plugin-nitro.js.map