From d6d35bcafcbe216caa1d9e8410bf2925a4d57467 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Tue, 15 Feb 2022 17:47:42 +0000 Subject: [PATCH] Support non-HTML pages (#2586) * adds support for build non-html pages * add non-html pages to the static build test suite * adds getStaticPaths() test for non-html pages * adds dev server tests for non-html pages * ading a changeset * updating changeset description * testing for building non-html files with async data * fixing typo in changeset docs --- .changeset/few-coats-warn.md | 44 ++++++++++ packages/astro/src/@types/astro.ts | 15 +++- .../astro/src/core/build/scan-based-build.ts | 24 ++++- packages/astro/src/core/build/static-build.ts | 87 +++++++++++-------- packages/astro/src/core/render/core.ts | 9 +- packages/astro/src/core/render/dev/index.ts | 17 ++-- .../astro/src/core/routing/manifest/create.ts | 15 ++-- packages/astro/src/runtime/server/index.ts | 16 +++- packages/astro/test/dev-routing.test.js | 60 +++++++++++++ .../src/pages/data/[slug].json.ts | 14 +++ .../static build/src/pages/company.json.ts | 8 ++ .../src/pages/data/[slug].json.ts | 16 ++++ .../static build/src/pages/posts.json.js | 22 +++++ .../src/pages/posts/nested/more.md | 1 + .../static build/src/pages/posts/thoughts.md | 1 + .../with-endpoint-routes/astro.config.mjs | 6 ++ .../src/pages/[slug].json.ts | 15 ++++ .../src/pages/data/[slug].json.ts | 15 ++++ .../src/pages/home.json.ts | 7 ++ packages/astro/test/static-build.test.js | 32 +++++++ 20 files changed, 369 insertions(+), 55 deletions(-) create mode 100644 .changeset/few-coats-warn.md create mode 100644 packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts create mode 100644 packages/astro/test/fixtures/static build/src/pages/company.json.ts create mode 100644 packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts create mode 100644 packages/astro/test/fixtures/static build/src/pages/posts.json.js create mode 100644 packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs create mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts create mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts create mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts diff --git a/.changeset/few-coats-warn.md b/.changeset/few-coats-warn.md new file mode 100644 index 000000000000..d02de6e3f411 --- /dev/null +++ b/.changeset/few-coats-warn.md @@ -0,0 +1,44 @@ +--- +'astro': patch +--- + +Support for non-HTML pages + +> ⚠️ This feature is currently only supported with the `--experimental-static-build` CLI flag. This feature may be refined over the next few weeks/months as SSR support is finalized. + +This adds support for generating non-HTML pages form `.js` and `.ts` pages during the build. Built file and extensions are based on the source file's name, ex: `src/pages/data.json.ts` will be built to `dist/data.json`. + +**Is this different from SSR?** Yes! This feature allows JSON, XML, etc. files to be output at build time. Keep an eye out for full SSR support if you need to build similar files when requested, for example as a serverless function in your deployment host. + +## Examples + +```typescript +// src/pages/company.json.ts +export async function get() { + return { + body: JSON.stringify({ + name: 'Astro Technology Company', + url: 'https://astro.build/' + }) + } +} +``` + +What about `getStaticPaths()`? It **just works**™. + +```typescript +export async function getStaticPaths() { + return [ + { params: { slug: 'thing1' }}, + { params: { slug: 'thing2' }} + ] +} + +export async function get(params) { + const { slug } = params + + return { + body: // ...JSON.stringify() + } +} +``` \ No newline at end of file diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 59853083681b..13fc013cbb9a 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -304,6 +304,17 @@ export interface RenderPageOptions { css?: string[]; } +type Body = string; + +export interface EndpointOutput { + body: Output; +} + +export interface EndpointHandler { + [method: string]: (params: any) => EndpointOutput; +} + + /** * Astro Renderer * Docs: https://docs.astro.build/reference/renderer-reference/ @@ -338,13 +349,15 @@ export interface Renderer { knownEntrypoints?: string[]; } +export type RouteType = 'page' | 'endpoint'; + export interface RouteData { component: string; generate: (data?: any) => string; params: string[]; pathname?: string; pattern: RegExp; - type: 'page'; + type: RouteType; } export type SerializedRouteData = Omit & { diff --git a/packages/astro/src/core/build/scan-based-build.ts b/packages/astro/src/core/build/scan-based-build.ts index e6d380b61c5e..d761e0bdb11e 100644 --- a/packages/astro/src/core/build/scan-based-build.ts +++ b/packages/astro/src/core/build/scan-based-build.ts @@ -1,6 +1,6 @@ import type { ViteDevServer } from '../vite.js'; -import type { AstroConfig } from '../../@types/astro'; -import type { AllPagesData } from './types'; +import type { AstroConfig, RouteType } from '../../@types/astro'; +import type { AllPagesData, PageBuildData } from './types'; import type { LogOptions } from '../logger'; import type { ViteConfigWithSSR } from '../create-vite.js'; @@ -22,6 +22,24 @@ export interface ScanBasedBuildOptions { viteServer: ViteDevServer; } +// Returns a filter predicate to filter AllPagesData entries by RouteType +function entryIsType(type: RouteType) { + return function withPage([_, pageData]: [string, PageBuildData]) { + return pageData.route.type === type; + }; +} + +// Reducer to combine AllPageData entries back into an object keyed by filepath +function reduceEntries(acc: { [key: string]: U }, [key, value]: [string, U]) { + acc[key] = value; + return acc; +} + +// Filters an AllPagesData object to only include routes of a specific RouteType +function routesOfType(type: RouteType, allPages: AllPagesData) { + return Object.entries(allPages).filter(entryIsType(type)).reduce(reduceEntries, {}); +} + export async function build(opts: ScanBasedBuildOptions) { const { allPages, astroConfig, logging, origin, pageNames, routeCache, viteConfig, viteServer } = opts; @@ -50,7 +68,7 @@ export async function build(opts: ScanBasedBuildOptions) { internals, logging, origin, - allPages, + allPages: routesOfType('page', allPages), pageNames, routeCache, viteServer, diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 49cdb750f8d6..1fc4765ec59e 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -1,6 +1,6 @@ import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup'; import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from '../vite'; -import type { AstroConfig, ComponentInstance, ManifestData, Renderer } from '../../@types/astro'; +import type { AstroConfig, EndpointHandler, ComponentInstance, ManifestData, Renderer, RouteType } from '../../@types/astro'; import type { AllPagesData } from './types'; import type { LogOptions } from '../logger'; import type { ViteConfigWithSSR } from '../create-vite'; @@ -122,28 +122,31 @@ export async function staticBuild(opts: StaticBuildOptions) { for (const [component, pageData] of Object.entries(allPages)) { const astroModuleURL = new URL('./' + component, astroConfig.projectRoot); const astroModuleId = prependForwardSlash(component); - const [renderers, mod] = pageData.preload; - const metadata = mod.$$metadata; - - const topLevelImports = new Set([ - // Any component that gets hydrated - ...metadata.hydratedComponentPaths(), - // Any hydration directive like astro/client/idle.js - ...metadata.hydrationDirectiveSpecifiers(), - // The client path for each renderer - ...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!), - ]); - - // Add hoisted scripts - const hoistedScripts = new Set(metadata.hoistedScriptPaths()); - if (hoistedScripts.size) { - const moduleId = npath.posix.join(astroModuleId, 'hoisted.js'); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts); - topLevelImports.add(moduleId); - } - for (const specifier of topLevelImports) { - jsInput.add(specifier); + if (pageData.route.type === 'page') { + const [renderers, mod] = pageData.preload; + const metadata = mod.$$metadata; + + const topLevelImports = new Set([ + // Any component that gets hydrated + ...metadata.hydratedComponentPaths(), + // Any hydration directive like astro/client/idle.js + ...metadata.hydrationDirectiveSpecifiers(), + // The client path for each renderer + ...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!), + ]); + + // Add hoisted scripts + const hoistedScripts = new Set(metadata.hoistedScriptPaths()); + if (hoistedScripts.size) { + const moduleId = npath.posix.join(astroModuleId, 'hoisted.js'); + internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts); + topLevelImports.add(moduleId); + } + + for (const specifier of topLevelImports) { + jsInput.add(specifier); + } } pageInput.add(astroModuleId); @@ -349,7 +352,9 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts; // This adds the page name to the array so it can be shown as part of stats. - addPageName(pathname, opts); + if (pageData.route.type === 'page') { + addPageName(pathname, opts); + } debug('build', `Generating: ${pathname}`); @@ -382,8 +387,8 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G site: astroConfig.buildOptions.site, }); - const outFolder = getOutFolder(astroConfig, pathname); - const outFile = getOutFile(astroConfig, outFolder, pathname); + const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type); + const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type); await fs.promises.mkdir(outFolder, { recursive: true }); await fs.promises.writeFile(outFile, html, 'utf-8'); } catch (err) { @@ -464,24 +469,34 @@ function getClientRoot(astroConfig: AstroConfig): URL { return serverFolder; } -function getOutFolder(astroConfig: AstroConfig, pathname: string): URL { +function getOutFolder(astroConfig: AstroConfig, pathname: string, routeType: RouteType): URL { const outRoot = getOutRoot(astroConfig); // This is the root folder to write to. - switch (astroConfig.buildOptions.pageUrlFormat) { - case 'directory': - return new URL('.' + appendForwardSlash(pathname), outRoot); - case 'file': + switch (routeType) { + case 'endpoint': return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot); + case 'page': + switch (astroConfig.buildOptions.pageUrlFormat) { + case 'directory': + return new URL('.' + appendForwardSlash(pathname), outRoot); + case 'file': + return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot); + } } } -function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string): URL { - switch (astroConfig.buildOptions.pageUrlFormat) { - case 'directory': - return new URL('./index.html', outFolder); - case 'file': - return new URL('./' + npath.basename(pathname) + '.html', outFolder); +function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL { + switch(routeType) { + case 'endpoint': + return new URL(npath.basename(pathname), outFolder); + case 'page': + switch (astroConfig.buildOptions.pageUrlFormat) { + case 'directory': + return new URL('./index.html', outFolder); + case 'file': + return new URL('./' + npath.basename(pathname) + '.html', outFolder); + } } } diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index db543ddb1990..aff319257a31 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,7 +1,7 @@ -import type { ComponentInstance, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro'; +import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro'; import type { LogOptions } from '../logger.js'; -import { renderPage } from '../../runtime/server/index.js'; +import { renderEndpoint, renderPage } from '../../runtime/server/index.js'; import { getParams } from '../routing/index.js'; import { createResult } from './result.js'; import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js'; @@ -74,6 +74,11 @@ export async function render(opts: RenderOptions): Promise { pathname, }); + // For endpoints, render the content immediately without injecting scripts or styles + if (route?.type === 'endpoint') { + return renderEndpoint(mod as any as EndpointHandler, params); + } + // Validate the page component before rendering the page const Component = await mod.default; if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index d2d245dc2abc..b589a0896960 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -64,7 +64,7 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO }); } - let html = await coreRender({ + let content = await coreRender({ experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, links: new Set(), logging, @@ -91,6 +91,11 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO site: astroConfig.buildOptions.site, }); + + if (route?.type === 'endpoint') { + return content; + } + // inject tags const tags: vite.HtmlTagDescriptor[] = []; @@ -128,20 +133,20 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO }); // add injected tags - html = injectTags(html, tags); + content = injectTags(content, tags); // run transformIndexHtml() in dev to run Vite dev transformations if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) { const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); - html = await viteServer.transformIndexHtml(relativeURL, html, pathname); + content = await viteServer.transformIndexHtml(relativeURL, content, pathname); } // inject if missing (TODO: is a more robust check needed for comments, etc.?) - if (!/\n' + html; + if (!/\n' + content; } - return html; + return content; } export async function ssr(ssrOpts: SSROptions): Promise { diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 70697a729fe2..909ddd1a55de 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -170,7 +170,8 @@ function comparator(a: Item, b: Item) { export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?: string }, logging: LogOptions): ManifestData { const components: string[] = []; const routes: RouteData[] = []; - const validExtensions: Set = new Set(['.astro', '.md']); + const validPageExtensions: Set = new Set(['.astro', '.md']); + const validEndpointExtensions: Set = new Set(['.js', '.ts']); function walk(dir: string, parentSegments: Part[][], parentParams: string[]) { let items: Item[] = []; @@ -189,7 +190,7 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd? return; } // filter out "foo.astro_tmp" files, etc - if (!isDir && !validExtensions.has(ext)) { + if (!isDir && !validPageExtensions.has(ext) && !validEndpointExtensions.has(ext)) { return; } const segment = isDir ? basename : name; @@ -209,6 +210,7 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd? const parts = getParts(segment, file); const isIndex = isDir ? false : basename.startsWith('index.'); const routeSuffix = basename.slice(basename.indexOf('.'), -ext.length); + const isPage = validPageExtensions.has(ext); items.push({ basename, @@ -217,7 +219,7 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd? file: slash(file), isDir, isIndex, - isPage: true, + isPage, routeSuffix, }); }); @@ -263,12 +265,13 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd? } else { components.push(item.file); const component = item.file; - const pattern = getPattern(segments, config.devOptions.trailingSlash); - const generate = getGenerator(segments, config.devOptions.trailingSlash); + const trailingSlash = item.isPage ? config.devOptions.trailingSlash : 'never'; + const pattern = getPattern(segments, trailingSlash); + const generate = getGenerator(segments, trailingSlash); const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; routes.push({ - type: 'page', + type: item.isPage ? 'page' : 'endpoint', pattern, params, component, diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index e987db5f69e9..56a6a2a499cb 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -1,4 +1,4 @@ -import type { AstroComponentMetadata, Renderer } from '../../@types/astro'; +import type { AstroComponentMetadata, EndpointHandler, Renderer } from '../../@types/astro'; import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/astro'; import shorthash from 'shorthash'; @@ -411,6 +411,20 @@ const uniqueElements = (item: any, index: number, all: any[]) => { return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children); }; +// Renders an endpoint request to completion, returning the body. +export async function renderEndpoint(mod: EndpointHandler, params: any) { + const method = 'get'; + const handler = mod[method]; + + if (!handler || typeof handler !== 'function') { + throw new Error(`Endpoint handler not found! Expected an exported function for "${method}"`); + } + + const { body } = await mod.get(params); + + return body; +} + // Renders a page to completion by first calling the factory callback, waiting for its result, and then appending // styles and scripts into the head. export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) { diff --git a/packages/astro/test/dev-routing.test.js b/packages/astro/test/dev-routing.test.js index 19844581813f..f30ff1abfdcc 100644 --- a/packages/astro/test/dev-routing.test.js +++ b/packages/astro/test/dev-routing.test.js @@ -171,4 +171,64 @@ describe('Development Routing', () => { expect(response.status).to.equal(500); }); }); + + describe('Endpoint routes', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + /** @type {import('./test-utils').DevServer} */ + let devServer; + + before(async () => { + fixture = await loadFixture({ projectRoot: './fixtures/with-endpoint-routes/' }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + devServer && (await devServer.stop()); + }); + + it('200 when loading /home.json', async () => { + const response = await fixture.fetch('/home.json'); + expect(response.status).to.equal(200); + + const body = await response.text().then((text) => JSON.parse(text)); + expect(body.title).to.equal('home'); + }); + + it('200 when loading /thing1.json', async () => { + const response = await fixture.fetch('/thing1.json'); + expect(response.status).to.equal(200); + + const body = await response.text().then((text) => JSON.parse(text)); + expect(body.slug).to.equal('thing1'); + expect(body.title).to.equal('[slug]'); + }); + + it('200 when loading /thing2.json', async () => { + const response = await fixture.fetch('/thing2.json'); + expect(response.status).to.equal(200); + + const body = await response.text().then((text) => JSON.parse(text)); + expect(body.slug).to.equal('thing2'); + expect(body.title).to.equal('[slug]'); + }); + + it('200 when loading /data/thing3.json', async () => { + const response = await fixture.fetch('/data/thing3.json'); + expect(response.status).to.equal(200); + + const body = await response.text().then((text) => JSON.parse(text)); + expect(body.slug).to.equal('thing3'); + expect(body.title).to.equal('data [slug]'); + }); + + it('200 when loading /data/thing4.json', async () => { + const response = await fixture.fetch('/data/thing4.json'); + expect(response.status).to.equal(200); + + const body = await response.text().then((text) => JSON.parse(text)); + expect(body.slug).to.equal('thing4'); + expect(body.title).to.equal('data [slug]'); + }); + }); }); diff --git a/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts b/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts new file mode 100644 index 000000000000..3c7cc63baa00 --- /dev/null +++ b/packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts @@ -0,0 +1,14 @@ +export async function getStaticPaths() { + return [ + { params: { slug: 'thing1' } }, + { params: { slug: 'thing2' } } + ]; +} + +export async function get() { + return { + body: JSON.stringify({ + title: '[slug]' + }, null, 4) + }; +} diff --git a/packages/astro/test/fixtures/static build/src/pages/company.json.ts b/packages/astro/test/fixtures/static build/src/pages/company.json.ts new file mode 100644 index 000000000000..ee3f2f1add77 --- /dev/null +++ b/packages/astro/test/fixtures/static build/src/pages/company.json.ts @@ -0,0 +1,8 @@ +export async function get() { + return { + body: JSON.stringify({ + name: 'Astro Technology Company', + url: 'https://astro.build/' + }) + } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts b/packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts new file mode 100644 index 000000000000..51a12db2e935 --- /dev/null +++ b/packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts @@ -0,0 +1,16 @@ +export async function getStaticPaths() { + return [ + { params: { slug: 'thing1' }}, + { params: { slug: 'thing2' }} + ] +} + +export async function get(params) { + return { + body: JSON.stringify({ + slug: params.slug, + name: 'Astro Technology Company', + url: 'https://astro.build/' + }) + } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/static build/src/pages/posts.json.js b/packages/astro/test/fixtures/static build/src/pages/posts.json.js new file mode 100644 index 000000000000..6463fdbad2b9 --- /dev/null +++ b/packages/astro/test/fixtures/static build/src/pages/posts.json.js @@ -0,0 +1,22 @@ +async function fetchPosts() { + const files = import.meta.glob('./posts/**/*.md'); + + const posts = await Promise.all( + Object.entries(files).map(([filename, load]) => load().then(({ frontmatter }) => { + return { + filename, + title: frontmatter.title, + }; + })), + ); + + return posts.sort((a, b) => a.title.localeCompare(b.title)); +} + +export async function get() { + const posts = await fetchPosts(); + + return { + body: JSON.stringify(posts, null, 4), + }; +} diff --git a/packages/astro/test/fixtures/static build/src/pages/posts/nested/more.md b/packages/astro/test/fixtures/static build/src/pages/posts/nested/more.md index b2304b74832e..5bfd7f3823d3 100644 --- a/packages/astro/test/fixtures/static build/src/pages/posts/nested/more.md +++ b/packages/astro/test/fixtures/static build/src/pages/posts/nested/more.md @@ -1,5 +1,6 @@ --- layout: ../../../layouts/Main.astro +title: More post --- # Post diff --git a/packages/astro/test/fixtures/static build/src/pages/posts/thoughts.md b/packages/astro/test/fixtures/static build/src/pages/posts/thoughts.md index 76e108103687..5edbf5e90c5e 100644 --- a/packages/astro/test/fixtures/static build/src/pages/posts/thoughts.md +++ b/packages/astro/test/fixtures/static build/src/pages/posts/thoughts.md @@ -1,5 +1,6 @@ --- layout: ../../layouts/Main.astro +title: Thoughts post --- # Post diff --git a/packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs b/packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs new file mode 100644 index 000000000000..7ac59b3413ea --- /dev/null +++ b/packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs @@ -0,0 +1,6 @@ + +export default { + buildOptions: { + site: 'http://example.com/' + } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts new file mode 100644 index 000000000000..b18660955bd7 --- /dev/null +++ b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts @@ -0,0 +1,15 @@ +export async function getStaticPaths() { + return [ + { params: { slug: 'thing1' } }, + { params: { slug: 'thing2' } } + ]; +} + +export async function get(params) { + return { + body: JSON.stringify({ + slug: params.slug, + title: '[slug]' + }) + }; +} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts new file mode 100644 index 000000000000..eea44d90b8f1 --- /dev/null +++ b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts @@ -0,0 +1,15 @@ +export async function getStaticPaths() { + return [ + { params: { slug: 'thing3' } }, + { params: { slug: 'thing4' } } + ]; +} + +export async function get(params) { + return { + body: JSON.stringify({ + slug: params.slug, + title: 'data [slug]' + }) + }; +} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts new file mode 100644 index 000000000000..8046af6dfc27 --- /dev/null +++ b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts @@ -0,0 +1,7 @@ +export async function get() { + return { + body: JSON.stringify({ + title: 'home' + }) + }; +} diff --git a/packages/astro/test/static-build.test.js b/packages/astro/test/static-build.test.js index 9accc4a66dd6..4ee9e9920a2f 100644 --- a/packages/astro/test/static-build.test.js +++ b/packages/astro/test/static-build.test.js @@ -42,6 +42,38 @@ describe('Static build', () => { expect(html).to.be.a('string'); }); + it('Builds out .json files', async () => { + const content = await fixture.readFile('/subpath/company.json').then((text) => JSON.parse(text)); + expect(content.name).to.equal('Astro Technology Company'); + expect(content.url).to.equal('https://astro.build/'); + }); + + it ('Builds out async .json files', async () => { + const content = await fixture.readFile('/subpath/posts.json').then((text) => JSON.parse(text)); + expect(Array.isArray(content)).to.equal(true); + expect(content).deep.equal([ + { + filename: './posts/nested/more.md', + title: 'More post', + }, + { + filename: './posts/thoughts.md', + title: 'Thoughts post', + }, + ]); + }); + + it('Builds out dynamic .json files', async () => { + const slugs = ['thing1', 'thing2']; + + for (const slug of slugs) { + const content = await fixture.readFile(`/subpath/data/${slug}.json`).then((text) => JSON.parse(text)); + expect(content.name).to.equal('Astro Technology Company'); + expect(content.url).to.equal('https://astro.build/'); + expect(content.slug).to.equal(slug); + } + }); + function createFindEvidence(expected) { return async function findEvidence(pathname) { const html = await fixture.readFile(pathname);