Handle layouts and head injection (#1007)
* Revert "Revert "Fixes duplicate head content (#995)" (#1001)"
This reverts commit b1959f0fed
.
* Handle markdown layouts
* Adding a changeset
This commit is contained in:
parent
08b442dcf5
commit
4bbf113050
17 changed files with 156 additions and 82 deletions
5
.changeset/kind-crabs-marry.md
Normal file
5
.changeset/kind-crabs-marry.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes a previous revert, makes sure head content is injected into the right place
|
|
@ -662,7 +662,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
|
||||||
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
|
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
|
||||||
paren++;
|
paren++;
|
||||||
}
|
}
|
||||||
buffers[curr] += `h("${name}", ${generateAttributes(attributes)}`;
|
buffers[curr] += `h("${name}", ${generateAttributes(attributes)},`;
|
||||||
paren++;
|
paren++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -692,7 +692,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
|
||||||
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
|
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
|
||||||
paren++;
|
paren++;
|
||||||
}
|
}
|
||||||
buffers[curr] += `h(${componentName}, ${generateAttributes(attributes)}`;
|
buffers[curr] += `h(${componentName}, ${generateAttributes(attributes)},`;
|
||||||
paren++;
|
paren++;
|
||||||
return;
|
return;
|
||||||
} else if (!componentInfo && !isCustomElementTag(componentName)) {
|
} else if (!componentInfo && !isCustomElementTag(componentName)) {
|
||||||
|
|
|
@ -150,8 +150,8 @@ async function __render(props, ...children) {
|
||||||
value: props,
|
value: props,
|
||||||
enumerable: true
|
enumerable: true
|
||||||
},
|
},
|
||||||
css: {
|
pageCSS: {
|
||||||
value: (props[__astroInternal] && props[__astroInternal].css) || [],
|
value: (props[__astroContext] && props[__astroContext].pageCSS) || [],
|
||||||
enumerable: true
|
enumerable: true
|
||||||
},
|
},
|
||||||
isPage: {
|
isPage: {
|
||||||
|
@ -179,18 +179,21 @@ export async function __renderPage({request, children, props, css}) {
|
||||||
__render,
|
__render,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperty(props, __astroContext, {
|
const isLayout = (__astroContext in props);
|
||||||
value: {
|
if(!isLayout) {
|
||||||
request
|
Object.defineProperty(props, __astroContext, {
|
||||||
},
|
value: {
|
||||||
writable: false,
|
pageCSS: css,
|
||||||
enumerable: false
|
request
|
||||||
});
|
},
|
||||||
|
writable: false,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(props, __astroInternal, {
|
Object.defineProperty(props, __astroInternal, {
|
||||||
value: {
|
value: {
|
||||||
css,
|
isPage: !isLayout
|
||||||
isPage: true
|
|
||||||
},
|
},
|
||||||
writable: false,
|
writable: false,
|
||||||
enumerable: false
|
enumerable: false
|
||||||
|
@ -202,7 +205,7 @@ export async function __renderPage({request, children, props, css}) {
|
||||||
if (currentChild.layout) {
|
if (currentChild.layout) {
|
||||||
return currentChild.layout({
|
return currentChild.layout({
|
||||||
request,
|
request,
|
||||||
props: {content: currentChild.content},
|
props: {content: currentChild.content, [__astroContext]: props[__astroContext]},
|
||||||
children: [childBodyResult],
|
children: [childBodyResult],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function (opts: TransformOptions): Transformer {
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
type: 'Expression',
|
type: 'Expression',
|
||||||
codeChunks: ['Astro.css.map(css => (', '))'],
|
codeChunks: ['Astro.pageCSS.map(css => (', '))'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
type: 'Element',
|
type: 'Element',
|
||||||
|
@ -162,22 +162,15 @@ export default function (opts: TransformOptions): Transformer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditionalNode = {
|
if (eoh.foundHeadOrHtmlElement || eoh.foundHeadAndBodyContent) {
|
||||||
start: 0,
|
const topLevelFragment = {
|
||||||
end: 0,
|
start: 0,
|
||||||
type: 'Expression',
|
end: 0,
|
||||||
codeChunks: ['Astro.isPage ? (', ') : null'],
|
type: 'Fragment',
|
||||||
children: [
|
children,
|
||||||
{
|
};
|
||||||
start: 0,
|
eoh.append(topLevelFragment);
|
||||||
end: 0,
|
}
|
||||||
type: 'Fragment',
|
|
||||||
children,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
eoh.append(conditionalNode);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,44 @@
|
||||||
import type { TemplateNode } from '@astrojs/parser';
|
import type { TemplateNode } from '@astrojs/parser';
|
||||||
|
|
||||||
const validHeadElements = new Set(['!doctype', 'title', 'meta', 'link', 'style', 'script', 'noscript', 'base']);
|
const beforeHeadElements = new Set(['!doctype', 'html']);
|
||||||
|
const validHeadElements = new Set(['title', 'meta', 'link', 'style', 'script', 'noscript', 'base']);
|
||||||
|
|
||||||
export class EndOfHead {
|
export class EndOfHead {
|
||||||
|
private html: TemplateNode | null = null;
|
||||||
private head: TemplateNode | null = null;
|
private head: TemplateNode | null = null;
|
||||||
private firstNonHead: TemplateNode | null = null;
|
private firstNonHead: TemplateNode | null = null;
|
||||||
private parent: TemplateNode | null = null;
|
private parent: TemplateNode | null = null;
|
||||||
private stack: TemplateNode[] = [];
|
private stack: TemplateNode[] = [];
|
||||||
|
|
||||||
|
public foundHeadElements = false;
|
||||||
|
public foundBodyElements = false;
|
||||||
public append: (...node: TemplateNode[]) => void = () => void 0;
|
public append: (...node: TemplateNode[]) => void = () => void 0;
|
||||||
|
|
||||||
get found(): boolean {
|
get found(): boolean {
|
||||||
return !!(this.head || this.firstNonHead);
|
return !!(this.head || this.firstNonHead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get foundHeadContent(): boolean {
|
||||||
|
return !!this.head || this.foundHeadElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
get foundHeadAndBodyContent(): boolean {
|
||||||
|
return this.foundHeadContent && this.foundBodyElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
get foundHeadOrHtmlElement(): boolean {
|
||||||
|
return !!(this.html || this.head);
|
||||||
|
}
|
||||||
|
|
||||||
enter(node: TemplateNode) {
|
enter(node: TemplateNode) {
|
||||||
|
const name = node.name ? node.name.toLowerCase() : null;
|
||||||
|
|
||||||
if (this.found) {
|
if (this.found) {
|
||||||
|
if (!validHeadElements.has(name)) {
|
||||||
|
if (node.type === 'Element') {
|
||||||
|
this.foundBodyElements = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,8 +49,6 @@ export class EndOfHead {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = node.name.toLowerCase();
|
|
||||||
|
|
||||||
if (name === 'head') {
|
if (name === 'head') {
|
||||||
this.head = node;
|
this.head = node;
|
||||||
this.parent = this.stack[this.stack.length - 2];
|
this.parent = this.stack[this.stack.length - 2];
|
||||||
|
@ -35,11 +56,24 @@ export class EndOfHead {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip !doctype and html elements
|
||||||
|
if (beforeHeadElements.has(name)) {
|
||||||
|
if (name === 'html') {
|
||||||
|
this.html = node;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validHeadElements.has(name)) {
|
if (!validHeadElements.has(name)) {
|
||||||
|
if (node.type === 'Element') {
|
||||||
|
this.foundBodyElements = true;
|
||||||
|
}
|
||||||
this.firstNonHead = node;
|
this.firstNonHead = node;
|
||||||
this.parent = this.stack[this.stack.length - 2];
|
this.parent = this.stack[this.stack.length - 2];
|
||||||
this.append = this.prependToFirstNonHead;
|
this.append = this.prependToFirstNonHead;
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
this.foundHeadElements = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,13 @@
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { suite } from 'uvu';
|
import { suite } from 'uvu';
|
||||||
import * as assert from 'uvu/assert';
|
import * as assert from 'uvu/assert';
|
||||||
import { loadConfig } from '#astro/config';
|
import { doc } from './test-utils.js';
|
||||||
import { createRuntime } from '#astro/runtime';
|
import { setup } from './helpers.js';
|
||||||
|
|
||||||
const DType = suite('doctype');
|
const DType = suite('doctype');
|
||||||
|
|
||||||
let runtime, setupError;
|
setup(DType, './fixtures/astro-doctype');
|
||||||
|
|
||||||
DType.before(async () => {
|
DType('Automatically prepends the standards mode doctype', async ({ runtime }) => {
|
||||||
try {
|
|
||||||
const astroConfig = await loadConfig(fileURLToPath(new URL('./fixtures/astro-doctype', import.meta.url)));
|
|
||||||
|
|
||||||
const logging = {
|
|
||||||
level: 'error',
|
|
||||||
dest: process.stderr,
|
|
||||||
};
|
|
||||||
|
|
||||||
runtime = await createRuntime(astroConfig, { logging });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
setupError = err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
DType.after(async () => {
|
|
||||||
(await runtime) && runtime.shutdown();
|
|
||||||
});
|
|
||||||
|
|
||||||
DType('No errors creating a runtime', () => {
|
|
||||||
assert.equal(setupError, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
DType('Automatically prepends the standards mode doctype', async () => {
|
|
||||||
const result = await runtime.load('/prepend');
|
const result = await runtime.load('/prepend');
|
||||||
assert.ok(!result.error, `build error: ${result.error}`);
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
@ -40,7 +15,7 @@ DType('Automatically prepends the standards mode doctype', async () => {
|
||||||
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
||||||
});
|
});
|
||||||
|
|
||||||
DType('No attributes added when doctype is provided by user', async () => {
|
DType('No attributes added when doctype is provided by user', async ({ runtime }) => {
|
||||||
const result = await runtime.load('/provided');
|
const result = await runtime.load('/provided');
|
||||||
assert.ok(!result.error, `build error: ${result.error}`);
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
@ -48,7 +23,7 @@ DType('No attributes added when doctype is provided by user', async () => {
|
||||||
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
||||||
});
|
});
|
||||||
|
|
||||||
DType.skip('Preserves user provided doctype', async () => {
|
DType.skip('Preserves user provided doctype', async ({ runtime }) => {
|
||||||
const result = await runtime.load('/preserve');
|
const result = await runtime.load('/preserve');
|
||||||
assert.ok(!result.error, `build error: ${result.error}`);
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
@ -56,7 +31,7 @@ DType.skip('Preserves user provided doctype', async () => {
|
||||||
assert.ok(html.startsWith('<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'), 'Doctype included was preserved');
|
assert.ok(html.startsWith('<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'), 'Doctype included was preserved');
|
||||||
});
|
});
|
||||||
|
|
||||||
DType('User provided doctype is case insensitive', async () => {
|
DType('User provided doctype is case insensitive', async ({ runtime }) => {
|
||||||
const result = await runtime.load('/capital');
|
const result = await runtime.load('/capital');
|
||||||
assert.ok(!result.error, `build error: ${result.error}`);
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
@ -65,4 +40,34 @@ DType('User provided doctype is case insensitive', async () => {
|
||||||
assert.not.ok(html.includes('</!DOCTYPE>'), 'There should not be a closing tag');
|
assert.not.ok(html.includes('</!DOCTYPE>'), 'There should not be a closing tag');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DType('Doctype can be provided in a layout', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/in-layout');
|
||||||
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
const html = result.contents.toString('utf-8');
|
||||||
|
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||||
|
|
||||||
|
const $ = doc(html);
|
||||||
|
assert.equal($('head link').length, 1, 'A link inside of the head');
|
||||||
|
});
|
||||||
|
|
||||||
|
DType('Doctype is added in a layout without one', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/in-layout-no-doctype');
|
||||||
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
const html = result.contents.toString('utf-8');
|
||||||
|
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||||
|
});
|
||||||
|
|
||||||
|
DType('Doctype is added in a layout used with markdown pages', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/in-layout-article');
|
||||||
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
const html = result.contents.toString('utf-8');
|
||||||
|
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||||
|
|
||||||
|
const $ = doc(html);
|
||||||
|
assert.equal($('head link').length, 1, 'A link inside of the head');
|
||||||
|
});
|
||||||
|
|
||||||
DType.run();
|
DType.run();
|
||||||
|
|
5
packages/astro/test/fixtures/astro-doctype/src/components/Meta.astro
vendored
Normal file
5
packages/astro/test/fixtures/astro-doctype/src/components/Meta.astro
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
import SubMeta from './SubMeta.astro';
|
||||||
|
---
|
||||||
|
<meta name="author" content="Astro Fan">
|
||||||
|
<SubMeta />
|
1
packages/astro/test/fixtures/astro-doctype/src/components/SubMeta.astro
vendored
Normal file
1
packages/astro/test/fixtures/astro-doctype/src/components/SubMeta.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta name="keywords" content="JavaScript,Astro">
|
14
packages/astro/test/fixtures/astro-doctype/src/layouts/WithDoctype.astro
vendored
Normal file
14
packages/astro/test/fixtures/astro-doctype/src/layouts/WithDoctype.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import Meta from '../components/Meta.astro';
|
||||||
|
---
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>My App</title>
|
||||||
|
<Meta />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
11
packages/astro/test/fixtures/astro-doctype/src/layouts/WithoutDoctype.astro
vendored
Normal file
11
packages/astro/test/fixtures/astro-doctype/src/layouts/WithoutDoctype.astro
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
import '../styles/global.css'
|
||||||
|
---
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>My App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
7
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout-article.md
vendored
Normal file
7
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout-article.md
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
layout: ../layouts/WithDoctype.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
# Some Title
|
||||||
|
|
||||||
|
Some content
|
4
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout-no-doctype.astro
vendored
Normal file
4
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout-no-doctype.astro
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
import WithoutDoctype from '../layouts/WithoutDoctype.astro';
|
||||||
|
---
|
||||||
|
<WithoutDoctype />
|
4
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout.astro
vendored
Normal file
4
packages/astro/test/fixtures/astro-doctype/src/pages/in-layout.astro
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
import WithDoctype from '../layouts/WithDoctype.astro';
|
||||||
|
---
|
||||||
|
<WithDoctype />
|
3
packages/astro/test/fixtures/astro-doctype/src/styles/global.css
vendored
Normal file
3
packages/astro/test/fixtures/astro-doctype/src/styles/global.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
body {
|
||||||
|
background: green;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
import Something from '../components/Something.jsx';
|
import Something from '../components/Something.jsx';
|
||||||
import Child from '../components/Child.astro';
|
import Child from '../components/Child.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<title>My page</title>
|
<title>My page</title>
|
||||||
<style>
|
<style>
|
||||||
.h1 {
|
.h1 {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
import Something from '../components/Something.jsx';
|
|
||||||
import Child from '../components/Child.astro';
|
|
||||||
---
|
|
||||||
<Something client:load />
|
|
||||||
<Child />
|
|
|
@ -25,14 +25,4 @@ NoHeadEl('Places style and scripts before the first non-head element', async ({
|
||||||
assert.equal($('script[src="/_snowpack/hmr-client.js"]').length, 1, 'Only the hmr client for the page');
|
assert.equal($('script[src="/_snowpack/hmr-client.js"]').length, 1, 'Only the hmr client for the page');
|
||||||
});
|
});
|
||||||
|
|
||||||
NoHeadEl('Injects HMR script even when there are no elements on the page', async ({ runtime }) => {
|
|
||||||
const result = await runtime.load('/no-elements');
|
|
||||||
assert.ok(!result.error, `build error: ${result.error}`);
|
|
||||||
|
|
||||||
const html = result.contents;
|
|
||||||
const $ = doc(html);
|
|
||||||
|
|
||||||
assert.equal($('script[src="/_snowpack/hmr-client.js"]').length, 1, 'Only the hmr client for the page');
|
|
||||||
});
|
|
||||||
|
|
||||||
NoHeadEl.run();
|
NoHeadEl.run();
|
||||||
|
|
Loading…
Reference in a new issue