diff --git a/.changeset/giant-ravens-look.md b/.changeset/giant-ravens-look.md new file mode 100644 index 000000000000..dfa5d6d95bee --- /dev/null +++ b/.changeset/giant-ravens-look.md @@ -0,0 +1,50 @@ +--- +'astro': minor +--- + +Adds a new `astro:routes:resolved` hook to the Integration API. Also update the `astro:build:done` hook by deprecating `routes` and adding a new `assets` map. + +When building an integration, you can now get access to routes inside the `astro:routes:resolved` hook: + +```js +const integration = () => { + return { + name: 'my-integration', + hooks: { + 'astro:routes:resolved': ({ routes }) => { + console.log(routes) + } + } + } +} +``` + +This hook runs before `astro:config:done`, and whenever a route changes in development. + +The `routes` array from `astro:build:done` is now deprecated, and exposed properties are now available on `astro:routes:resolved`, except for `distURL`. For this, you can use the newly exposed `assets` map: + +```diff +const integration = () => { ++ let routes + return { + name: 'my-integration', + hooks: { ++ 'astro:routes:resolved': (params) => { ++ routes = params.routes ++ }, + 'astro:build:done': ({ +- routes ++ assets + }) => { ++ for (const route of routes) { ++ const distURL = assets.get(route.pattern) ++ if (distURL) { ++ Object.assign(route, { distURL }) ++ } ++ } + console.log(routes) + } + } + } +} +``` \ No newline at end of file diff --git a/packages/astro/src/actions/integration.ts b/packages/astro/src/actions/integration.ts index d7c99fc421e3..13d76e8b6023 100644 --- a/packages/astro/src/actions/integration.ts +++ b/packages/astro/src/actions/integration.ts @@ -18,10 +18,11 @@ export default function astroIntegrationActionsRouteHandler({ name: VIRTUAL_MODULE_ID, hooks: { async 'astro:config:setup'(params) { - params.injectRoute({ + settings.injectedRoutes.push({ pattern: ACTION_RPC_ROUTE_PATTERN, entrypoint: 'astro/actions/runtime/route.js', prerender: false, + origin: 'internal', }); params.addMiddleware({ diff --git a/packages/astro/src/assets/endpoint/config.ts b/packages/astro/src/assets/endpoint/config.ts index a910df25cc62..b9309d446997 100644 --- a/packages/astro/src/assets/endpoint/config.ts +++ b/packages/astro/src/assets/endpoint/config.ts @@ -63,5 +63,6 @@ function getImageEndpointData( pathname: settings.config.image.endpoint.route, prerender: false, fallbackRoutes: [], + origin: 'internal', }; } diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index 7694f3afa24b..2564e28750ac 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -544,6 +544,7 @@ export class experimental_AstroContainer { type, fallbackRoutes: [], isIndex: false, + origin: 'internal', }; } diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index 3f9090498be2..0ca40cd74b3a 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -7,6 +7,7 @@ import * as vite from 'vite'; import { runHookConfigDone, runHookConfigSetup, + runHookRoutesResolved, runHookServerDone, runHookServerStart, } from '../../integrations/hooks.js'; @@ -83,10 +84,11 @@ export async function createContainer({ .filter(Boolean) as string[]; // Create the route manifest already outside of Vite so that `runHookConfigDone` can use it to inform integrations of the build output - let manifest = await createRouteManifest({ settings, fsMod: fs }, logger); + let manifest = await createRouteManifest({ settings, fsMod: fs }, logger, { dev: true }); const devSSRManifest = createDevelopmentManifest(settings); manifest = injectDefaultDevRoutes(settings, devSSRManifest, manifest); + await runHookRoutesResolved({ settings, logger, routes: manifest.routes }); await runHookConfigDone({ settings, logger, command: 'dev' }); diff --git a/packages/astro/src/core/routing/astro-designed-error-pages.ts b/packages/astro/src/core/routing/astro-designed-error-pages.ts index 671221b5d68a..4348e6e974f0 100644 --- a/packages/astro/src/core/routing/astro-designed-error-pages.ts +++ b/packages/astro/src/core/routing/astro-designed-error-pages.ts @@ -15,6 +15,7 @@ export const DEFAULT_404_ROUTE: RouteData = { route: '/404', fallbackRoutes: [], isIndex: false, + origin: 'internal', }; export const DEFAULT_500_ROUTE: RouteData = { @@ -29,6 +30,7 @@ export const DEFAULT_500_ROUTE: RouteData = { route: '/500', fallbackRoutes: [], isIndex: false, + origin: 'internal', }; export function ensure404Route(manifest: ManifestData) { diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 33b5216998db..621164084e9f 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -20,6 +20,7 @@ import { routeComparator } from '../priority.js'; import { getRouteGenerator } from './generator.js'; import { getPattern } from './pattern.js'; import { getRoutePrerenderOption } from './prerender.js'; +import { runHookRoutesResolved } from '../../../integrations/hooks.js'; const require = createRequire(import.meta.url); interface Item { @@ -255,6 +256,7 @@ function createFileBasedRoutes( prerender, fallbackRoutes: [], distURL: [], + origin: 'project', }); } } @@ -280,7 +282,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou const routes: RouteData[] = []; for (const injectedRoute of settings.injectedRoutes) { - const { pattern: name, entrypoint, prerender: prerenderInjected } = injectedRoute; + const { pattern: name, entrypoint, prerender: prerenderInjected, origin } = injectedRoute; const { resolved, component } = resolveInjectedRoute(entrypoint.toString(), config.root, cwd); const segments = removeLeadingForwardSlash(name) @@ -320,6 +322,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou prerender: prerenderInjected ?? prerender, fallbackRoutes: [], distURL: [], + origin, }); } @@ -389,6 +392,7 @@ function createRedirectRoutes( redirectRoute: routeMap.get(destination), fallbackRoutes: [], distURL: [], + origin: 'project', }); } @@ -480,6 +484,7 @@ function detectRouteCollision(a: RouteData, b: RouteData, _config: AstroConfig, export async function createRouteManifest( params: CreateRouteManifestParams, logger: Logger, + { dev = false }: { dev?: boolean } = {}, ): Promise { const { settings } = params; const { config } = settings; @@ -727,6 +732,10 @@ export async function createRouteManifest( } } + if (!dev) { + await runHookRoutesResolved({ routes, settings, logger }); + } + return { routes, }; diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts index c0cf600f0b79..3d6214876c00 100644 --- a/packages/astro/src/core/routing/manifest/serialization.ts +++ b/packages/astro/src/core/routing/manifest/serialization.ts @@ -41,5 +41,6 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa return deserializeRouteData(fallback); }), isIndex: rawRouteData.isIndex, + origin: rawRouteData.origin, }; } diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index 540f75b843e6..999826a6f8a7 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -31,6 +31,7 @@ export function getServerIslandRouteData(config: ConfigFields) { isIndex: false, fallbackRoutes: [], route: SERVER_ISLAND_ROUTE, + origin: 'internal', }; return route; } diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 4c1d4a1ae2c1..5a4b723b2000 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -24,7 +24,9 @@ import type { import type { AstroIntegration, AstroRenderer, + BaseIntegrationHooks, HookParameters, + IntegrationResolvedRoute, IntegrationRouteData, RouteOptions, } from '../types/public/integrations.js'; @@ -39,7 +41,7 @@ async function withTakingALongTimeMsg({ logger, }: { name: string; - hookName: string; + hookName: keyof BaseIntegrationHooks; hookResult: T | Promise; timeoutMs?: number; logger: Logger; @@ -204,7 +206,7 @@ export async function runHookConfigSetup({ ); injectRoute.entrypoint = injectRoute.entryPoint as string; } - updatedSettings.injectedRoutes.push(injectRoute); + updatedSettings.injectedRoutes.push({ ...injectRoute, origin: 'external' }); }, addWatchFile: (path) => { updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path); @@ -599,6 +601,9 @@ export async function runHookBuildDone({ settings, pages, routes, logging }: Run pages: pages.map((p) => ({ pathname: p })), dir, routes: integrationRoutes, + assets: new Map( + routes.filter((r) => r.distURL !== undefined).map((r) => [r.route, r.distURL!]), + ), logger, }), logger: logging, @@ -648,6 +653,47 @@ export async function runHookRouteSetup({ } } +export async function runHookRoutesResolved({ + routes, + settings, + logger, +}: { routes: Array; settings: AstroSettings; logger: Logger }) { + for (const integration of settings.config.integrations) { + if (integration?.hooks?.['astro:routes:resolved']) { + const integrationLogger = getLogger(integration, logger); + + await withTakingALongTimeMsg({ + name: integration.name, + hookName: 'astro:routes:resolved', + hookResult: integration.hooks['astro:routes:resolved']({ + routes: routes.map((route) => toIntegrationResolvedRoute(route)), + logger: integrationLogger, + }), + logger, + }); + } + } +} + +function toIntegrationResolvedRoute(route: RouteData): IntegrationResolvedRoute { + return { + isPrerendered: route.prerender, + entrypoint: route.component, + pattern: route.route, + params: route.params, + origin: route.origin, + generate: route.generate, + patternRegex: route.pattern, + segments: route.segments, + type: route.type, + pathname: route.pathname, + redirect: route.redirect, + redirectRoute: route.redirectRoute + ? toIntegrationResolvedRoute(route.redirectRoute) + : undefined, + }; +} + function toIntegrationRouteData(route: RouteData): IntegrationRouteData { return { route: route.route, diff --git a/packages/astro/src/types/astro.ts b/packages/astro/src/types/astro.ts index 7e2f2af7e073..654652c422b3 100644 --- a/packages/astro/src/types/astro.ts +++ b/packages/astro/src/types/astro.ts @@ -10,12 +10,10 @@ import type { ContentEntryType, DataEntryType } from './public/content.js'; import type { AstroAdapter, AstroRenderer, - InjectedRoute, InjectedScriptStage, InjectedType, - ResolvedInjectedRoute, } from './public/integrations.js'; -import type { RouteData } from './public/internal.js'; +import type { InternalInjectedRoute, RouteData, ResolvedInjectedRoute } from './public/internal.js'; import type { DevToolbarAppEntry } from './public/toolbar.js'; export type SerializedRouteData = Omit< @@ -35,7 +33,7 @@ export interface AstroSettings { config: AstroConfig; adapter: AstroAdapter | undefined; preferences: AstroPreferences; - injectedRoutes: InjectedRoute[]; + injectedRoutes: InternalInjectedRoute[]; resolvedInjectedRoutes: ResolvedInjectedRoute[]; pageExtensions: string[]; contentEntryTypes: ContentEntryType[]; diff --git a/packages/astro/src/types/public/integrations.ts b/packages/astro/src/types/public/integrations.ts index 4d0709fab468..2d0b13e96300 100644 --- a/packages/astro/src/types/public/integrations.ts +++ b/packages/astro/src/types/public/integrations.ts @@ -8,7 +8,7 @@ import type { getToolbarServerCommunicationHelpers } from '../../integrations/ho import type { DeepPartial } from '../../type-utils.js'; import type { AstroConfig } from './config.js'; import type { RefreshContentOptions } from './content.js'; -import type { RouteData } from './internal.js'; +import type { InternalInjectedRoute, RouteData } from './internal.js'; import type { DevToolbarAppEntry } from './toolbar.js'; export interface RouteOptions { @@ -138,15 +138,7 @@ export type AstroAdapterFeatureMap = { */ export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr'; -export interface InjectedRoute { - pattern: string; - entrypoint: string | URL; - prerender?: boolean; -} - -export interface ResolvedInjectedRoute extends InjectedRoute { - resolvedEntryPoint?: URL; -} +export type InjectedRoute = Omit; export interface InjectedType { filename: string; @@ -225,13 +217,19 @@ export interface BaseIntegrationHooks { 'astro:build:done': (options: { pages: { pathname: string }[]; dir: URL; + /** @deprecated Use the `assets` map and the new `astro:routes:resolved` hook */ routes: IntegrationRouteData[]; + assets: Map; logger: AstroIntegrationLogger; }) => void | Promise; 'astro:route:setup': (options: { route: RouteOptions; logger: AstroIntegrationLogger; }) => void | Promise; + 'astro:routes:resolved': (options: { + routes: IntegrationResolvedRoute[]; + logger: AstroIntegrationLogger; + }) => void | Promise; } export interface AstroIntegration { @@ -245,13 +243,45 @@ export interface AstroIntegration { /** * A smaller version of the {@link RouteData} that is used in the integrations. + * @deprecated Use {@link IntegrationResolvedRoute} */ export type IntegrationRouteData = Omit< RouteData, - 'isIndex' | 'fallbackRoutes' | 'redirectRoute' + 'isIndex' | 'fallbackRoutes' | 'redirectRoute' | 'origin' > & { /** * {@link RouteData.redirectRoute} */ redirectRoute?: IntegrationRouteData; }; + +export interface IntegrationResolvedRoute + extends Pick< + RouteData, + 'generate' | 'params' | 'pathname' | 'segments' | 'type' | 'redirect' | 'origin' + > { + /** + * {@link RouteData.route} + */ + pattern: RouteData['route']; + + /** + * {@link RouteData.pattern} + */ + patternRegex: RouteData['pattern']; + + /** + * {@link RouteData.component} + */ + entrypoint: RouteData['component']; + + /** + * {@link RouteData.prerender} + */ + isPrerendered: RouteData['prerender']; + + /** + * {@link RouteData.redirectRoute} + */ + redirectRoute?: IntegrationResolvedRoute; +} diff --git a/packages/astro/src/types/public/internal.ts b/packages/astro/src/types/public/internal.ts index c970ab3d18ac..a17ee76506a0 100644 --- a/packages/astro/src/types/public/internal.ts +++ b/packages/astro/src/types/public/internal.ts @@ -136,6 +136,11 @@ export interface RouteData { * - src/pages/blog/index.astro */ isIndex: boolean; + + /** + * Whether the route comes from Astro core, an integration or the user's project + */ + origin: 'internal' | 'external' | 'project'; } /** @@ -284,3 +289,16 @@ export interface SSRMetadata { } export type SSRError = Error & ViteErrorPayload['err']; + +// `origin` is set within the hook, but the user doesn't have access to this property. That's why +// we need an intermediary interface +export interface InternalInjectedRoute { + pattern: string; + entrypoint: string | URL; + prerender?: boolean; + origin: RouteData['origin']; +} + +export interface ResolvedInjectedRoute extends InternalInjectedRoute { + resolvedEntryPoint?: URL; +} diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 2a22db6c7729..642565161b61 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import type fs from 'node:fs'; import { IncomingMessage } from 'node:http'; import type * as vite from 'vite'; +import { normalizePath } from 'vite'; import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js'; import { warnMissingAdapter } from '../core/dev/adapter-validation.js'; import { createKey } from '../core/encryption.js'; @@ -21,6 +22,9 @@ import { recordServerError } from './error.js'; import { DevPipeline } from './pipeline.js'; import { handleRequest } from './request.js'; import { setRouteError } from './server-state.js'; +import { fileURLToPath } from 'node:url'; +import { getRoutePrerenderOption } from '../core/routing/manifest/prerender.js'; +import { runHookRoutesResolved } from '../integrations/hooks.js'; export interface AstroPluginOptions { settings: AstroSettings; @@ -50,26 +54,46 @@ export default function createVitePluginAstroServer({ const controller = createController({ loader }); const localStorage = new AsyncLocalStorage(); - /** rebuild the route cache + manifest, as needed. */ - async function rebuildManifest(needsManifestRebuild: boolean) { + /** rebuild the route cache + manifest */ + async function rebuildManifest(path: string | null = null) { pipeline.clearRouteCache(); - if (needsManifestRebuild) { + + // If a route changes, we check if it's part of the manifest and check for its prerender value + if (path !== null) { + const route = routeManifest.routes.find( + (r) => + normalizePath(path) === + normalizePath(fileURLToPath(new URL(r.component, settings.config.root))), + ); + if (!route) { + return; + } + if (route.type !== 'page' && route.type !== 'endpoint') return; + + const routePath = fileURLToPath(new URL(route.component, settings.config.root)); + try { + const content = await fsMod.promises.readFile(routePath, 'utf-8'); + await getRoutePrerenderOption(content, route, settings, logger); + } catch (_) {} + } else { routeManifest = injectDefaultDevRoutes( settings, devSSRManifest, - await createRouteManifest({ settings, fsMod }, logger), // TODO: Handle partial updates to the manifest + await createRouteManifest({ settings, fsMod }, logger, { dev: true }), ); - warnMissingAdapter(logger, settings); - pipeline.manifest.checkOrigin = - settings.config.security.checkOrigin && settings.buildOutput === 'server'; - pipeline.setManifestData(routeManifest); } + await runHookRoutesResolved({ routes: routeManifest.routes, settings, logger }); + + warnMissingAdapter(logger, settings); + pipeline.manifest.checkOrigin = + settings.config.security.checkOrigin && settings.buildOutput === 'server'; + pipeline.setManifestData(routeManifest); } - // Rebuild route manifest on file change, if needed. - viteServer.watcher.on('add', rebuildManifest.bind(null, true)); - viteServer.watcher.on('unlink', rebuildManifest.bind(null, true)); - viteServer.watcher.on('change', rebuildManifest.bind(null, false)); + // Rebuild route manifest on file change + viteServer.watcher.on('add', rebuildManifest.bind(null, null)); + viteServer.watcher.on('unlink', rebuildManifest.bind(null, null)); + viteServer.watcher.on('change', rebuildManifest); function handleUnhandledRejection(rejection: any) { const error = new AstroError({ diff --git a/packages/astro/src/vite-plugin-integrations-container/index.ts b/packages/astro/src/vite-plugin-integrations-container/index.ts index 0af181e39c55..81b720aef10c 100644 --- a/packages/astro/src/vite-plugin-integrations-container/index.ts +++ b/packages/astro/src/vite-plugin-integrations-container/index.ts @@ -5,7 +5,7 @@ import type { AstroSettings } from '../types/astro.js'; import { normalizePath } from 'vite'; import { runHookServerSetup } from '../integrations/hooks.js'; -import type { InjectedRoute, ResolvedInjectedRoute } from '../types/public/integrations.js'; +import type { InternalInjectedRoute, ResolvedInjectedRoute } from '../types/public/internal.js'; /** Connect Astro integrations into Vite, as needed. */ export default function astroIntegrationsContainerPlugin({ @@ -33,7 +33,7 @@ export default function astroIntegrationsContainerPlugin({ async function resolveEntryPoint( this: PluginContext, - route: InjectedRoute, + route: InternalInjectedRoute, ): Promise { const resolvedId = await this.resolve(route.entrypoint.toString()) .then((res) => res?.id) diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js index f2365c006b39..97da20c19332 100644 --- a/packages/astro/test/units/integrations/api.test.js +++ b/packages/astro/test/units/integrations/api.test.js @@ -7,7 +7,7 @@ import { runHookBuildSetup, runHookConfigSetup, } from '../../../dist/integrations/hooks.js'; -import { defaultLogger } from '../test-utils.js'; +import { createFixture, defaultLogger, runInContainer } from '../test-utils.js'; const defaultConfig = { root: new URL('./', import.meta.url), @@ -131,6 +131,221 @@ describe('Integration API', () => { assert.equal(updatedSettings.config.site, site); assert.equal(updatedSettings.config.integrations.length, 2); }); + + describe('Routes resolved hooks', () => { + it('should work in dev', async () => { + let routes = []; + const fixture = await createFixture({ + '/src/pages/about.astro': '', + '/src/actions.ts': 'export const server = {}', + '/src/foo.astro': '', + }); + + await runInContainer( + { + inlineConfig: { + root: fixture.path, + integrations: [ + { + name: 'test', + hooks: { + 'astro:config:setup': (params) => { + params.injectRoute({ + entrypoint: './src/foo.astro', + pattern: '/foo', + }); + }, + 'astro:routes:resolved': (params) => { + routes = params.routes.map((r) => ({ + isPrerendered: r.isPrerendered, + entrypoint: r.entrypoint, + pattern: r.pattern, + params: r.params, + origin: r.origin, + })); + routes.sort((a, b) => a.pattern.localeCompare(b.pattern)); + }, + }, + }, + ], + }, + }, + async (container) => { + assert.deepEqual( + routes, + [ + { + isPrerendered: false, + entrypoint: '_server-islands.astro', + pattern: '/_server-islands/[name]', + params: ['name'], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/actions/runtime/route.js', + pattern: '/_actions/[...path]', + params: ['...path'], + origin: 'internal', + }, + { + isPrerendered: true, + entrypoint: 'src/pages/about.astro', + pattern: '/about', + params: [], + origin: 'project', + }, + { + isPrerendered: true, + entrypoint: 'src/foo.astro', + pattern: '/foo', + params: [], + origin: 'external', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/assets/endpoint/node.js', + pattern: '/_image', + params: [], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: 'astro-default-404.astro', + pattern: '/404', + params: [], + origin: 'internal', + }, + ].sort((a, b) => a.pattern.localeCompare(b.pattern)), + ); + + await fixture.writeFile('/src/pages/bar.astro', ''); + container.viteServer.watcher.emit( + 'add', + fixture.getPath('/src/pages/bar.astro').replace(/\\/g, '/'), + ); + await new Promise((r) => setTimeout(r, 100)); + + assert.deepEqual( + routes, + [ + { + isPrerendered: false, + entrypoint: '_server-islands.astro', + pattern: '/_server-islands/[name]', + params: ['name'], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/actions/runtime/route.js', + pattern: '/_actions/[...path]', + params: ['...path'], + origin: 'internal', + }, + { + isPrerendered: true, + entrypoint: 'src/pages/about.astro', + pattern: '/about', + params: [], + origin: 'project', + }, + { + isPrerendered: true, + entrypoint: 'src/pages/bar.astro', + pattern: '/bar', + params: [], + origin: 'project', + }, + { + isPrerendered: true, + entrypoint: 'src/foo.astro', + pattern: '/foo', + params: [], + origin: 'external', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/assets/endpoint/node.js', + pattern: '/_image', + params: [], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: 'astro-default-404.astro', + pattern: '/404', + params: [], + origin: 'internal', + }, + ].sort((a, b) => a.pattern.localeCompare(b.pattern)), + ); + + await fixture.writeFile('/src/pages/about.astro', '---\nexport const prerender=false\n'); + container.viteServer.watcher.emit( + 'change', + fixture.getPath('/src/pages/about.astro').replace(/\\/g, '/'), + ); + await new Promise((r) => setTimeout(r, 100)); + + assert.deepEqual( + routes, + [ + { + isPrerendered: false, + entrypoint: '_server-islands.astro', + pattern: '/_server-islands/[name]', + params: ['name'], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/actions/runtime/route.js', + pattern: '/_actions/[...path]', + params: ['...path'], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: 'src/pages/about.astro', + pattern: '/about', + params: [], + origin: 'project', + }, + { + isPrerendered: true, + entrypoint: 'src/pages/bar.astro', + pattern: '/bar', + params: [], + origin: 'project', + }, + { + isPrerendered: true, + entrypoint: 'src/foo.astro', + pattern: '/foo', + params: [], + origin: 'external', + }, + { + isPrerendered: false, + entrypoint: '../../../../dist/assets/endpoint/node.js', + pattern: '/_image', + params: [], + origin: 'internal', + }, + { + isPrerendered: false, + entrypoint: 'astro-default-404.astro', + pattern: '/404', + params: [], + origin: 'internal', + }, + ].sort((a, b) => a.pattern.localeCompare(b.pattern)), + ); + }, + ); + }); + }); }); describe('Astro feature map', function () {