Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements build.format: 'preserve' #9764

Merged
merged 11 commits into from
Jan 31, 2024
14 changes: 14 additions & 0 deletions .changeset/tame-flies-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'astro': minor
---

Adds a new possible value to the `build.format` configuration: 'preserve'. It will preserve your source structure in the final build

Using `build.format: 'file'`, a method to produce HTML files that are *not* all within folders, it will only produce `index.html` for the base path of `/`. This meant that even if you create explicit index pages with, for example, `page/index.astro`, it would write these out as `page.html`.

This is a bit unexpected, but rather than make a breaking change to `build.format: 'file'` we decided to create a new `build.format: 'preserve'`.

The new format will preserve how the filesystem is structured and make sure that is mirrored over to production. Using this option:

- `page.astro` becomes `page.html`
- `page/index.astro` becomes `page/index.html`
ematipico marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 4 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,12 +733,13 @@ export interface AstroUserConfig {
* Control the output file format of each page. This value may be set by an adapter for you.
* - If `'file'`, Astro will generate an HTML file (ex: "/foo.html") for each page.
* - If `'directory'`, Astro will generate a directory with a nested `index.html` file (ex: "/foo/index.html") for each page.
* - If `'preserve'`, Astro will generate HTML files exactly as they appear in your source folder (ex: "foo/index.astro" becomes "foo/index.html" but "foo.astro" does not)
ematipico marked this conversation as resolved.
Show resolved Hide resolved
*
* ```js
* {
* build: {
* // Example: Generate `page.html` instead of `page/index.html` during build.
* format: 'file'
* format: 'preserve'
* }
* }
* ```
Expand All @@ -756,7 +757,7 @@ export interface AstroUserConfig {
* - `directory` - Set `trailingSlash: 'always'`
* - `file` - Set `trailingSlash: 'never'`
*/
format?: 'file' | 'directory';
format?: 'file' | 'directory' | 'preserve';
/**
* @docs
* @name build.client
Expand Down Expand Up @@ -2556,6 +2557,7 @@ export interface RouteData {
redirect?: RedirectConfig;
redirectRoute?: RouteData;
fallbackRoutes: RouteData[];
isIndex: boolean;
}

export type RedirectRouteData = RouteData & {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type SSRManifest = {
site?: string;
base: string;
trailingSlash: 'always' | 'never' | 'ignore';
buildFormat: 'file' | 'directory';
buildFormat: 'file' | 'directory' | 'preserve';
compressHTML: boolean;
assetsPrefix?: string;
renderers: SSRLoadedRenderer[];
Expand Down
28 changes: 25 additions & 3 deletions packages/astro/src/core/build/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import npath from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AstroConfig, RouteType } from '../../@types/astro.js';
import type { AstroConfig, RouteData } from '../../@types/astro.js';
import { appendForwardSlash } from '../../core/path.js';

const STATUS_CODE_PAGES = new Set(['/404', '/500']);
Expand All @@ -17,9 +17,10 @@ function getOutRoot(astroConfig: AstroConfig): URL {
export function getOutFolder(
astroConfig: AstroConfig,
pathname: string,
routeType: RouteType
routeData: RouteData
): URL {
const outRoot = getOutRoot(astroConfig);
const routeType = routeData.type;

// This is the root folder to write to.
switch (routeType) {
Expand All @@ -39,6 +40,17 @@ export function getOutFolder(
const d = pathname === '' ? pathname : npath.dirname(pathname);
return new URL('.' + appendForwardSlash(d), outRoot);
}
case 'preserve': {
let dir;
// If the pathname is '' then this is the root index.html
// If this is an index route, the folder should be the pathname, not the parent
if(pathname === '' || routeData.isIndex) {
dir = pathname;
} else {
dir = npath.dirname(pathname);
}
return new URL('.' + appendForwardSlash(dir), outRoot);
}
}
}
}
Expand All @@ -47,8 +59,9 @@ export function getOutFile(
astroConfig: AstroConfig,
outFolder: URL,
pathname: string,
routeType: RouteType
routeData: RouteData
): URL {
const routeType = routeData.type;
switch (routeType) {
case 'endpoint':
return new URL(npath.basename(pathname), outFolder);
Expand All @@ -67,6 +80,15 @@ export function getOutFile(
const baseName = npath.basename(pathname);
return new URL('./' + (baseName || 'index') + '.html', outFolder);
}
case 'preserve': {
let baseName = npath.basename(pathname);
// If there is no base name this is the root route.
// If this is an index route, the name should be `index.html`.
if(!baseName || routeData.isIndex) {
baseName = 'index';
}
return new URL(`./${baseName}.html`, outFolder);
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ function getUrlForPath(
pathname: string,
base: string,
origin: string,
format: 'directory' | 'file',
format: 'directory' | 'file' | 'preserve',
routeType: RouteType
): URL {
/**
Expand Down Expand Up @@ -602,8 +602,8 @@ async function generatePath(
body = Buffer.from(await response.arrayBuffer());
}

const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
const outFolder = getOutFolder(pipeline.getConfig(), pathname, route);
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route);
route.distURL = outFile;

await fs.promises.mkdir(outFolder, { recursive: true });
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ function buildManifest(
if (!route.prerender) continue;
if (!route.pathname) continue;

const outFolder = getOutFolder(opts.settings.config, route.pathname, route.type);
const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route.type);
const outFolder = getOutFolder(opts.settings.config, route.pathname, route);
const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route);
const file = outFile.toString().replace(opts.settings.config.build.client.toString(), '');
routes.push({
file,
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function shouldAppendForwardSlash(
switch (buildFormat) {
case 'directory':
return true;
case 'preserve':
case 'file':
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const AstroConfigSchema = z.object({
build: z
.object({
format: z
.union([z.literal('file'), z.literal('directory')])
.union([z.literal('file'), z.literal('directory'), z.literal('preserve')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.format),
client: z
Expand Down Expand Up @@ -465,7 +465,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
build: z
.object({
format: z
.union([z.literal('file'), z.literal('directory')])
.union([z.literal('file'), z.literal('directory'), z.literal('preserve')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.format),
client: z
Expand Down
19 changes: 8 additions & 11 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ interface Item {
routeSuffix: string;
}

interface ManifestRouteData extends RouteData {
isIndex: boolean;
}

function countOccurrences(needle: string, haystack: string) {
let count = 0;
for (const hay of haystack) {
Expand Down Expand Up @@ -193,7 +189,8 @@ function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]
* For example, `/bar` is sorted before `/foo`.
* The definition of "alphabetically" is dependent on the default locale of the running system.
*/
function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {

function routeComparator(a: RouteData, b: RouteData) {
const commonLength = Math.min(a.segments.length, b.segments.length);

for (let index = 0; index < commonLength; index++) {
Expand Down Expand Up @@ -301,9 +298,9 @@ export interface CreateRouteManifestParams {
function createFileBasedRoutes(
{ settings, cwd, fsMod }: CreateRouteManifestParams,
logger: Logger
): ManifestRouteData[] {
): RouteData[] {
const components: string[] = [];
const routes: ManifestRouteData[] = [];
const routes: RouteData[] = [];
const validPageExtensions = new Set<string>([
'.astro',
...SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
Expand Down Expand Up @@ -444,7 +441,7 @@ function createFileBasedRoutes(
return routes;
}

type PrioritizedRoutesData = Record<RoutePriorityOverride, ManifestRouteData[]>;
type PrioritizedRoutesData = Record<RoutePriorityOverride, RouteData[]>;

function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): PrioritizedRoutesData {
const { config } = settings;
Expand Down Expand Up @@ -690,7 +687,7 @@ export function createRouteManifest(

const redirectRoutes = createRedirectRoutes(params, routeMap, logger);

const routes: ManifestRouteData[] = [
const routes: RouteData[] = [
...injectedRoutes['legacy'].sort(routeComparator),
...[...fileBasedRoutes, ...injectedRoutes['normal'], ...redirectRoutes['normal']].sort(
routeComparator
Expand Down Expand Up @@ -726,8 +723,8 @@ export function createRouteManifest(

// In this block of code we group routes based on their locale

// A map like: locale => ManifestRouteData[]
const routesByLocale = new Map<string, ManifestRouteData[]>();
// A map like: locale => RouteData[]
const routesByLocale = new Map<string, RouteData[]>();
// This type is here only as a helper. We copy the routes and make them unique, so we don't "process" the same route twice.
// The assumption is that a route in the file system belongs to only one locale.
const setRoutes = new Set(routes.filter((route) => route.type === 'page'));
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/routing/manifest/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa
fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
return deserializeRouteData(fallback);
}),
isIndex: rawRouteData.isIndex,
};
}
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export async function handleRoute({
type: 'fallback',
route: '',
fallbackRoutes: [],
isIndex: false,
};
renderContext = await createRenderContext({
request,
Expand Down
48 changes: 36 additions & 12 deletions packages/astro/test/astro-pageDirectoryUrl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@ import { expect } from 'chai';
import { loadFixture } from './test-utils.js';

describe('build format', () => {
let fixture;
describe('build.format: file', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-page-directory-url',
build: {
format: 'file',
},
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-page-directory-url',
build: {
format: 'file',
},
});
await fixture.build();
});

it('outputs', async () => {
expect(await fixture.readFile('/client.html')).to.be.ok;
expect(await fixture.readFile('/nested-md.html')).to.be.ok;
expect(await fixture.readFile('/nested-astro.html')).to.be.ok;
});
await fixture.build();
});

it('outputs', async () => {
expect(await fixture.readFile('/client.html')).to.be.ok;
expect(await fixture.readFile('/nested-md.html')).to.be.ok;
expect(await fixture.readFile('/nested-astro.html')).to.be.ok;
describe('build.format: preserve', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-page-directory-url',
build: {
format: 'preserve',
},
});
await fixture.build();
});

it('outputs', async () => {
expect(await fixture.readFile('/client.html')).to.be.ok;
expect(await fixture.readFile('/nested-md/index.html')).to.be.ok;
expect(await fixture.readFile('/nested-astro/index.html')).to.be.ok;
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---
<html>
<head><title>testing</title></head>
<body><h1>testing</h1></body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
const another = new URL('./another/', Astro.url);
---
<a id="another" href={another.pathname}></a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---
<html>
<head><title>testing</title></head>
<body><h1>testing</h1></body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>
41 changes: 41 additions & 0 deletions packages/astro/test/page-format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,45 @@ describe('build.format', () => {
});
});
});

describe('preserve', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
base: '/test',
root: './fixtures/page-format/',
trailingSlash: 'always',
build: {
format: 'preserve',
},
i18n: {
locales: ['en'],
defaultLocale: 'en',
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
}
}
});
});

describe('Build', () => {
before(async () => {
await fixture.build();
});

it('relative urls created point to sibling folders', async () => {
let html = await fixture.readFile('/en/nested/page.html');
let $ = cheerio.load(html);
expect($('#another').attr('href')).to.equal('/test/en/nested/another/');
});

it('index files are written as index.html', async () => {
let html = await fixture.readFile('/en/nested/index.html');
let $ = cheerio.load(html);
expect($('h1').text()).to.equal('Testing');
});
});
});
});
Loading