HTML minification (#6706)
* TDD pattern development * add compact property when the user run pnpm run build * add minification for pro * fix yaml file collision * fix yaml collision * fix pageage file * optimize unit test * fix revert code * fix comment * update yaml * fix default value * add test for dev * Update packages/astro/test/astro-minification-html.test.js Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update packages/astro/test/astro-minification-html.test.js Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update packages/astro/test/astro-minification-html.test.js Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update packages/astro/test/astro-minification-html.test.js Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update packages/astro/test/astro-minification-html.test.js Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update the docs to reflect it's opt-in * Add tests for SSR * Document how the tests remove the doctype line * Expand on the changeset * rename for slice -100 * Updates based on PR comments * optimize description * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> --------- Co-authored-by: wuls <linsheng.wu@beantechs.com> Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: Matthew Phillips <matthew@matthewphillips.info> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
5c3c672d26
commit
763ff2d1e4
12 changed files with 169 additions and 1 deletions
17
.changeset/tiny-snails-dance.md
Normal file
17
.changeset/tiny-snails-dance.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds an opt-in way to minify the HTML output.
|
||||||
|
|
||||||
|
Using the `compressHTML` option Astro will remove whitespace from Astro components. This only applies to components written in `.astro` format and happens in the compiler to maximize performance. You can enable with:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
compressHTML: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Compression occurs both in development mode and in the final build.
|
|
@ -466,6 +466,24 @@ export interface AstroUserConfig {
|
||||||
*/
|
*/
|
||||||
site?: string;
|
site?: string;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name compressHTML
|
||||||
|
* @type {boolean}
|
||||||
|
* @default `false`
|
||||||
|
* @description
|
||||||
|
* This is an option to minify your HTML output and reduce the size of your HTML files. When enabled, Astro removes all whitespace from your HTML, including line breaks, from `.astro` components. This occurs both in development mode and in the final build.
|
||||||
|
* To enable this, set the `compressHTML` flag to `true`.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* {
|
||||||
|
* compressHTML: true
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
compressHTML?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @name base
|
* @name base
|
||||||
|
|
|
@ -37,6 +37,7 @@ export async function compile({
|
||||||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||||
// result passed to esbuild, but also available in the catch handler.
|
// result passed to esbuild, but also available in the catch handler.
|
||||||
transformResult = await transform(source, {
|
transformResult = await transform(source, {
|
||||||
|
compact: astroConfig.compressHTML,
|
||||||
filename,
|
filename,
|
||||||
normalizedFilename: normalizeFilename(filename, astroConfig.root),
|
normalizedFilename: normalizeFilename(filename, astroConfig.root),
|
||||||
sourcemap: 'both',
|
sourcemap: 'both',
|
||||||
|
|
|
@ -23,6 +23,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
assets: '_astro',
|
assets: '_astro',
|
||||||
serverEntry: 'entry.mjs',
|
serverEntry: 'entry.mjs',
|
||||||
},
|
},
|
||||||
|
compressHTML: false,
|
||||||
server: {
|
server: {
|
||||||
host: false,
|
host: false,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
@ -72,6 +73,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
|
.default(ASTRO_CONFIG_DEFAULTS.cacheDir)
|
||||||
.transform((val) => new URL(val)),
|
.transform((val) => new URL(val)),
|
||||||
site: z.string().url().optional(),
|
site: z.string().url().optional(),
|
||||||
|
compressHTML: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.compressHTML),
|
||||||
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
|
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
|
||||||
trailingSlash: z
|
trailingSlash: z
|
||||||
.union([z.literal('always'), z.literal('never'), z.literal('ignore')])
|
.union([z.literal('always'), z.literal('never'), z.literal('ignore')])
|
||||||
|
@ -225,6 +227,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
||||||
.string()
|
.string()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.srcDir)
|
.default(ASTRO_CONFIG_DEFAULTS.srcDir)
|
||||||
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
|
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
|
||||||
|
compressHTML: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(ASTRO_CONFIG_DEFAULTS.compressHTML),
|
||||||
publicDir: z
|
publicDir: z
|
||||||
.string()
|
.string()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.publicDir)
|
.default(ASTRO_CONFIG_DEFAULTS.publicDir)
|
||||||
|
|
|
@ -79,7 +79,6 @@ export async function renderPage(
|
||||||
result._metadata.headInTree =
|
result._metadata.headInTree =
|
||||||
result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
|
result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
|
||||||
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
||||||
|
|
||||||
let output: ComponentIterable;
|
let output: ComponentIterable;
|
||||||
let head = '';
|
let head = '';
|
||||||
try {
|
try {
|
||||||
|
|
7
packages/astro/test/fixtures/minification-html/astro.config.mjs
vendored
Normal file
7
packages/astro/test/fixtures/minification-html/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
compressHTML: true,
|
||||||
|
|
||||||
|
});
|
8
packages/astro/test/fixtures/minification-html/package.json
vendored
Normal file
8
packages/astro/test/fixtures/minification-html/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/minification-html",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
3
packages/astro/test/fixtures/minification-html/src/pages/aside.astro
vendored
Normal file
3
packages/astro/test/fixtures/minification-html/src/pages/aside.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
<div>2</div>
|
21
packages/astro/test/fixtures/minification-html/src/pages/index.astro
vendored
Normal file
21
packages/astro/test/fixtures/minification-html/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
import Aside from './aside.astro'
|
||||||
|
import Page from './page.astro'
|
||||||
|
---
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>minimum html</title>
|
||||||
|
<style>
|
||||||
|
.body{
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Aside/>
|
||||||
|
<main></main>
|
||||||
|
<Page></Page>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
packages/astro/test/fixtures/minification-html/src/pages/page.astro
vendored
Normal file
3
packages/astro/test/fixtures/minification-html/src/pages/page.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
<div>3</div>
|
79
packages/astro/test/minification-html.test.js
Normal file
79
packages/astro/test/minification-html.test.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import testAdapter from './test-adapter.js';
|
||||||
|
|
||||||
|
const NEW_LINES = /[\r\n]+/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The doctype declaration is on a line between the rest of the HTML.
|
||||||
|
* This function removes the doctype so that we can check if the rest of the HTML is without
|
||||||
|
* whitespace.
|
||||||
|
*/
|
||||||
|
function removeDoctypeLine(html) {
|
||||||
|
return html.slice(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the dev environment, two more script tags will be injected than in the production environment
|
||||||
|
* so that we can check if the rest of the HTML is without whitespace
|
||||||
|
*/
|
||||||
|
function removeDoctypeLineInDev(html){
|
||||||
|
return html.slice(-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HTML minification', () => {
|
||||||
|
describe('in DEV enviroment', () => {
|
||||||
|
let fixture;
|
||||||
|
let devServer;
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/minification-html/',
|
||||||
|
});
|
||||||
|
devServer = await fixture.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit compressed HTML in the emitted file', async () => {
|
||||||
|
let res = await fixture.fetch(`/`);
|
||||||
|
expect(res.status).to.equal(200);
|
||||||
|
const html = await res.text();
|
||||||
|
expect(NEW_LINES.test(removeDoctypeLineInDev(html))).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Build SSG', () => {
|
||||||
|
let fixture;
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({ root: './fixtures/minification-html/' });
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit compressed HTML in the emitted file', async () => {
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
expect(NEW_LINES.test(removeDoctypeLine(html))).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Build SSR', () => {
|
||||||
|
let fixture;
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/minification-html/',
|
||||||
|
output: 'server',
|
||||||
|
adapter: testAdapter()
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit compressed HTML in the emitted file', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
expect(NEW_LINES.test(removeDoctypeLine(html))).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2810,6 +2810,12 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../..
|
version: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/minification-html:
|
||||||
|
dependencies:
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/multiple-renderers:
|
packages/astro/test/fixtures/multiple-renderers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@test/astro-renderer-one':
|
'@test/astro-renderer-one':
|
||||||
|
|
Loading…
Reference in a new issue