summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@skypack.dev> 2024-01-31 09:39:20 -0500
committerGravatar GitHub <noreply@github.com> 2024-01-31 14:39:20 +0000
commitfad4f64aa149086feda2d1f3a0b655767034f1a8 (patch)
tree87c42a0ea29c585b177cee7e34af3c79a388ab42
parent84c100dd33fc1fe5d6308419fa3d0e1e1ba07441 (diff)
downloadastro-fad4f64aa149086feda2d1f3a0b655767034f1a8.tar.gz
astro-fad4f64aa149086feda2d1f3a0b655767034f1a8.tar.zst
astro-fad4f64aa149086feda2d1f3a0b655767034f1a8.zip
Implements build.format: 'preserve' (#9764)
* Implements build.format: 'preserve' * Restructure test * Add a test for base * Update .changeset/tame-flies-confess.md Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * Add trailing slash + i18n testing * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/tame-flies-confess.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * tiny punctuation/conjunction nit fixes --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r--.changeset/tame-flies-confess.md18
-rw-r--r--packages/astro/src/@types/astro.ts10
-rw-r--r--packages/astro/src/core/app/types.ts2
-rw-r--r--packages/astro/src/core/build/common.ts28
-rw-r--r--packages/astro/src/core/build/generate.ts4
-rw-r--r--packages/astro/src/core/build/plugins/plugin-manifest.ts4
-rw-r--r--packages/astro/src/core/build/util.ts1
-rw-r--r--packages/astro/src/core/config/schema.ts4
-rw-r--r--packages/astro/src/core/routing/manifest/create.ts19
-rw-r--r--packages/astro/src/core/routing/manifest/serialization.ts1
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts1
-rw-r--r--packages/astro/test/astro-pageDirectoryUrl.test.js48
-rw-r--r--packages/astro/test/fixtures/page-format/src/pages/en/index.astro6
-rw-r--r--packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro8
-rw-r--r--packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro4
-rw-r--r--packages/astro/test/fixtures/page-format/src/pages/index.astro6
-rw-r--r--packages/astro/test/fixtures/page-format/src/pages/nested/index.astro8
-rw-r--r--packages/astro/test/page-format.test.js41
18 files changed, 176 insertions, 37 deletions
diff --git a/.changeset/tame-flies-confess.md b/.changeset/tame-flies-confess.md
new file mode 100644
index 000000000..7b10c2358
--- /dev/null
+++ b/.changeset/tame-flies-confess.md
@@ -0,0 +1,18 @@
+---
+'astro': minor
+---
+
+Adds a new `build.format` configuration option: 'preserve'. This option will preserve your source structure in the final build.
+
+The existing configuration options, `file` and `directory`, either build all of your HTML pages as files matching the route name (e.g. `/about.html`) or build all your files as `index.html` within a nested directory structure (e.g. `/about/index.html`), respectively. It was not previously possible to control the HTML file built on a per-file basis.
+
+One limitation of `build.format: 'file'` is that it cannot create `index.html` files for any individual routes (other than the base path of `/`) while otherwise building named files. Creating explicit index pages within your file structure still generates a file named for the page route (e.g. `src/pages/about/index.astro` builds `/about.html`) when using the `file` configuration option.
+
+Rather than make a breaking change to allow `build.format: 'file'` to be more flexible, 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:
+
+- `about.astro` becomes `about.html`
+- `about/index.astro` becomes `about/index.html`
+
+See the [`build.format` configuration options reference] for more details.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index c5cdfac1b..bc0ab1e3e 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -788,14 +788,15 @@ export interface AstroUserConfig {
* @default `'directory'`
* @description
* 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.
+ * - `'file'`: Astro will generate an HTML file named for each page route. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about.html`)
+ * - `'directory'`: Astro will generate a directory with a nested `index.html` file for each page. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about/index.html`)
+ * - `'preserve'`: Astro will generate HTML files exactly as they appear in your source folder. (e.g. `src/pages/about.astro` builds `/about.html` and `src/pages/about/index.astro` builds the file `/about/index.html`)
*
* ```js
* {
* build: {
* // Example: Generate `page.html` instead of `page/index.html` during build.
- * format: 'file'
+ * format: 'preserve'
* }
* }
* ```
@@ -813,7 +814,7 @@ export interface AstroUserConfig {
* - `directory` - Set `trailingSlash: 'always'`
* - `file` - Set `trailingSlash: 'never'`
*/
- format?: 'file' | 'directory';
+ format?: 'file' | 'directory' | 'preserve';
/**
* @docs
* @name build.client
@@ -2637,6 +2638,7 @@ export interface RouteData {
redirect?: RedirectConfig;
redirectRoute?: RouteData;
fallbackRoutes: RouteData[];
+ isIndex: boolean;
}
export type RedirectRouteData = RouteData & {
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 184f3b1d5..17b7c0872 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -41,7 +41,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[];
diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts
index e7efc6439..daa719a3e 100644
--- a/packages/astro/src/core/build/common.ts
+++ b/packages/astro/src/core/build/common.ts
@@ -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']);
@@ -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) {
@@ -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);
+ }
}
}
}
@@ -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);
@@ -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);
+ }
}
}
}
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 19f90176e..b108da5f5 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -622,8 +622,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 });
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index 0c2717f4e..e5a1c1b06 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -172,8 +172,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,
diff --git a/packages/astro/src/core/build/util.ts b/packages/astro/src/core/build/util.ts
index fde296a6d..96a5ec2f2 100644
--- a/packages/astro/src/core/build/util.ts
+++ b/packages/astro/src/core/build/util.ts
@@ -23,6 +23,7 @@ export function shouldAppendForwardSlash(
switch (buildFormat) {
case 'directory':
return true;
+ case 'preserve':
case 'file':
return false;
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 255360eea..95f33eb41 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -128,7 +128,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
@@ -539,7 +539,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
diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts
index d27d0cc40..3818f08c7 100644
--- a/packages/astro/src/core/routing/manifest/create.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -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) {
@@ -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++) {
@@ -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,
@@ -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;
@@ -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
@@ -727,8 +724,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'));
diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts
index f70aa84dd..431febcb8 100644
--- a/packages/astro/src/core/routing/manifest/serialization.ts
+++ b/packages/astro/src/core/routing/manifest/serialization.ts
@@ -38,5 +38,6 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa
fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
return deserializeRouteData(fallback);
}),
+ isIndex: rawRouteData.isIndex,
};
}
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 35d4b7278..9f9951b7e 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -235,6 +235,7 @@ export async function handleRoute({
type: 'fallback',
route: '',
fallbackRoutes: [],
+ isIndex: false,
};
renderContext = await createRenderContext({
request,
diff --git a/packages/astro/test/astro-pageDirectoryUrl.test.js b/packages/astro/test/astro-pageDirectoryUrl.test.js
index 978db056a..19b75e222 100644
--- a/packages/astro/test/astro-pageDirectoryUrl.test.js
+++ b/packages/astro/test/astro-pageDirectoryUrl.test.js
@@ -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;
+ });
});
});
diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/index.astro b/packages/astro/test/fixtures/page-format/src/pages/en/index.astro
new file mode 100644
index 000000000..bcd4c7539
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/index.astro
@@ -0,0 +1,6 @@
+---
+---
+<html>
+ <head><title>testing</title></head>
+ <body><h1>testing</h1></body>
+</html>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro b/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro
new file mode 100644
index 000000000..9c077e2a3
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Testing</title>
+ </head>
+ <body>
+ <h1>Testing</h1>
+ </body>
+</html>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro b/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro
new file mode 100644
index 000000000..eb67508a7
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro
@@ -0,0 +1,4 @@
+---
+const another = new URL('./another/', Astro.url);
+---
+<a id="another" href={another.pathname}></a>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/index.astro b/packages/astro/test/fixtures/page-format/src/pages/index.astro
new file mode 100644
index 000000000..bcd4c7539
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/index.astro
@@ -0,0 +1,6 @@
+---
+---
+<html>
+ <head><title>testing</title></head>
+ <body><h1>testing</h1></body>
+</html>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro b/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro
new file mode 100644
index 000000000..9c077e2a3
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Testing</title>
+ </head>
+ <body>
+ <h1>Testing</h1>
+ </body>
+</html>
diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.js
index 2143bf09b..63e5dae83 100644
--- a/packages/astro/test/page-format.test.js
+++ b/packages/astro/test/page-format.test.js
@@ -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');
+ });
+ });
+ });
});