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
This commit is contained in:
parent
b8dbba6c4b
commit
d6d35bcafc
20 changed files with 367 additions and 53 deletions
44
.changeset/few-coats-warn.md
Normal file
44
.changeset/few-coats-warn.md
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
```
|
|
@ -304,6 +304,17 @@ export interface RenderPageOptions {
|
|||
css?: string[];
|
||||
}
|
||||
|
||||
type Body = string;
|
||||
|
||||
export interface EndpointOutput<Output extends Body = Body> {
|
||||
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<RouteData, 'generate' | 'pattern'> & {
|
||||
|
|
|
@ -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<U>(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,
|
||||
|
|
|
@ -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!),
|
||||
]);
|
||||
if (pageData.route.type === 'page') {
|
||||
const [renderers, mod] = pageData.preload;
|
||||
const metadata = mod.$$metadata;
|
||||
|
||||
// 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);
|
||||
}
|
||||
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!),
|
||||
]);
|
||||
|
||||
for (const specifier of topLevelImports) {
|
||||
jsInput.add(specifier);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string> {
|
|||
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}`);
|
||||
|
|
|
@ -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 <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
html = '<!DOCTYPE html>\n' + html;
|
||||
if (!/<!doctype html/i.test(content)) {
|
||||
content = '<!DOCTYPE html>\n' + content;
|
||||
}
|
||||
|
||||
return html;
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function ssr(ssrOpts: SSROptions): Promise<string> {
|
||||
|
|
|
@ -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<string> = new Set(['.astro', '.md']);
|
||||
const validPageExtensions: Set<string> = new Set(['.astro', '.md']);
|
||||
const validEndpointExtensions: Set<string> = 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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
14
packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts
vendored
Normal file
14
packages/astro/test/fixtures/astro-get-static-paths/src/pages/data/[slug].json.ts
vendored
Normal file
|
@ -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)
|
||||
};
|
||||
}
|
8
packages/astro/test/fixtures/static build/src/pages/company.json.ts
vendored
Normal file
8
packages/astro/test/fixtures/static build/src/pages/company.json.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
export async function get() {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
name: 'Astro Technology Company',
|
||||
url: 'https://astro.build/'
|
||||
})
|
||||
}
|
||||
}
|
16
packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts
vendored
Normal file
16
packages/astro/test/fixtures/static build/src/pages/data/[slug].json.ts
vendored
Normal file
|
@ -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/'
|
||||
})
|
||||
}
|
||||
}
|
22
packages/astro/test/fixtures/static build/src/pages/posts.json.js
vendored
Normal file
22
packages/astro/test/fixtures/static build/src/pages/posts.json.js
vendored
Normal file
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: ../../../layouts/Main.astro
|
||||
title: More post
|
||||
---
|
||||
|
||||
# Post
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: ../../layouts/Main.astro
|
||||
title: Thoughts post
|
||||
---
|
||||
|
||||
# Post
|
||||
|
|
6
packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs
vendored
Normal file
6
packages/astro/test/fixtures/with-endpoint-routes/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
export default {
|
||||
buildOptions: {
|
||||
site: 'http://example.com/'
|
||||
}
|
||||
}
|
15
packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts
vendored
Normal file
15
packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts
vendored
Normal file
|
@ -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]'
|
||||
})
|
||||
};
|
||||
}
|
15
packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts
vendored
Normal file
15
packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts
vendored
Normal file
|
@ -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]'
|
||||
})
|
||||
};
|
||||
}
|
7
packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts
vendored
Normal file
7
packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
export async function get() {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
title: 'home'
|
||||
})
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue