Skip to content

Commit

Permalink
Add .html support (#3867)
Browse files Browse the repository at this point in the history
* feat: add html package

* feat: support assets in HTML

* feat(html): upgrade html integration

* feat(html): add `@astrojs/html` integration

* feat(html): add html support to astro core

* test(html): update html tests with package.json files

* chore: add changeset

* fix: remove import cycle

* chore: fix types

* refactor: remove @astrojs/html, add to core

* chore: update types for `*.html`

* fix: move *.html to astro/env

Co-authored-by: Nate Moore <nate@astro.build>
  • Loading branch information
natemoo-re and natemoo-re authored Jul 22, 2022
1 parent 8b468cc commit 7250e4e
Show file tree
Hide file tree
Showing 32 changed files with 534 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-stingrays-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Add support for `.html` components and pages
5 changes: 5 additions & 0 deletions packages/astro/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ declare module '*.md' {
const load: MD['default'];
export default load;
}

declare module "*.html" {
const Component: { render(opts: { slots: Record<string, string> }): string };
export default Component;
}
3 changes: 3 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"prismjs": "^1.28.0",
"prompts": "^2.4.2",
"recast": "^0.20.5",
"rehype": "^12.0.1",
"resolve": "^1.22.0",
"rollup": "^2.75.6",
"semver": "^7.3.7",
Expand All @@ -136,6 +137,8 @@
"strip-ansi": "^7.0.1",
"supports-esm": "^1.0.0",
"tsconfig-resolver": "^3.0.1",
"unist-util-visit": "^4.1.0",
"vfile": "^5.3.2",
"vite": "3.0.2",
"yargs-parser": "^21.0.1",
"zod": "^3.17.3"
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ export async function validateConfig(
const result = {
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
_ctx: {
pageExtensions: ['.astro', '.md'],
pageExtensions: ['.astro', '.md', '.html'],
scripts: [],
renderers: [],
injectedRoutes: [],
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import envVitePlugin from '../vite-plugin-env/index.js';
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import htmlVitePlugin from '../vite-plugin-html/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import { createCustomViteLogger } from './errors.js';
Expand Down Expand Up @@ -73,6 +74,7 @@ export async function createVite(
mode === 'dev' && astroViteServerPlugin({ config: astroConfig, logging }),
envVitePlugin({ config: astroConfig }),
markdownVitePlugin({ config: astroConfig }),
htmlVitePlugin(),
jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig }),
astroIntegrationsContainerPlugin({ config: astroConfig }),
Expand Down
9 changes: 1 addition & 8 deletions packages/astro/src/core/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,7 @@ export default async function dev(config: AstroConfig, options: DevOptions): Pro
mode: 'development',
server: { host },
optimizeDeps: {
include: [
'astro/client/idle.js',
'astro/client/load.js',
'astro/client/visible.js',
'astro/client/media.js',
'astro/client/only.js',
...rendererClientEntries,
],
include: rendererClientEntries,
},
},
{ astroConfig: config, logging: options.logging, mode: 'dev' }
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export function fixViteErrorMessage(_err: unknown, server: ViteDevServer, filePa
const content = fs.readFileSync(fileURLToPath(filePath)).toString();
const lns = content.split('\n');
const line = lns.findIndex((ln) => ln.includes(importName));
const column = lns[line].indexOf(importName);
if (line == -1) return err;
const column = lns[line]?.indexOf(importName);
if (!(err as any).id) {
(err as any).id = `${fileURLToPath(filePath)}:${line + 1}:${column + 1}`;
}
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,21 @@ export async function renderComponent(
return markHTMLString(children);
}

if (Component && typeof Component === 'object' && (Component as any)['astro:html']) {
const children: Record<string, string> = {};
if (slots) {
await Promise.all(
Object.entries(slots).map(([key, value]) =>
renderSlot(result, value as string).then((output) => {
children[key] = output;
})
)
);
}
const html = (Component as any).render({ slots: children });
return markHTMLString(html);
}

if (Component && (Component as any).isAstroComponentFactory) {
async function* renderAstroComponentInline(): AsyncGenerator<string, void, undefined> {
let iterable = await renderToIterable(result, Component as any, _props, slots);
Expand Down Expand Up @@ -265,6 +280,7 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
)
);
}

// Call the renderers `check` hook to see if any claim this component.
let renderer: SSRLoadedRenderer | undefined;
if (metadata.hydrate !== 'only') {
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/src/vite-plugin-html/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { transform } from './transform/index.js';

export default function html() {
return {
name: 'astro:html',
options(options: any) {
options.plugins = options.plugins?.filter((p: any) => p.name !== 'vite:build-html');
},
async transform(source: string, id: string) {
if (!id.endsWith('.html')) return;
return await transform(source, id);
}
}
}
27 changes: 27 additions & 0 deletions packages/astro/src/vite-plugin-html/transform/escape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Plugin } from 'unified';
import type { Root, RootContent } from 'hast';
import type MagicString from 'magic-string';
import { visit } from 'unist-util-visit';

import { replaceAttribute, needsEscape, escape } from './utils.js';

const rehypeEscape: Plugin<[{ s: MagicString }], Root> = ({ s }) => {
return (tree, file) => {
visit(tree, (node: Root | RootContent, index, parent) => {
if (node.type === 'text' || node.type === 'comment') {
if (needsEscape(node.value)) {
s.overwrite(node.position!.start.offset!, node.position!.end.offset!, escape(node.value));
}
} else if (node.type === 'element') {
for (const [key, value] of Object.entries(node.properties ?? {})) {
const newKey = needsEscape(key) ? escape(key) : key;
const newValue = needsEscape(value) ? escape(value) : value;
if (newKey === key && newValue === value) continue;
replaceAttribute(s, node, key, (value === '') ? newKey : `${newKey}="${newValue}"`);
}
}
});
};
};

export default rehypeEscape;
32 changes: 32 additions & 0 deletions packages/astro/src/vite-plugin-html/transform/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import MagicString from 'magic-string';
import { rehype } from 'rehype';
import { VFile } from 'vfile';
import escape from './escape.js';
import slots, { SLOT_PREFIX } from './slots.js';

export async function transform(code: string, id: string) {
const s = new MagicString(code, { filename: id });
const imports = new Map();
const parser = rehype()
.data('settings', { fragment: true })
.use(escape, { s })
.use(slots, { s });

const vfile = new VFile({ value: code, path: id })
await parser.process(vfile)
s.prepend(`export default {\n\t"astro:html": true,\n\trender({ slots: ${SLOT_PREFIX} }) {\n\t\treturn \``);
s.append('`\n\t}\n}');

if (imports.size > 0) {
let importText = ''
for (const [path, importName] of imports.entries()) {
importText += `import ${importName} from "${path}";\n`
}
s.prepend(importText);
}

return {
code: s.toString(),
map: s.generateMap()
}
}
27 changes: 27 additions & 0 deletions packages/astro/src/vite-plugin-html/transform/slots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Plugin } from 'unified';
import type { Root, RootContent } from 'hast';

import { visit } from 'unist-util-visit';
import MagicString from 'magic-string';
import { escape } from './utils.js';

const rehypeSlots: Plugin<[{ s: MagicString }], Root> = ({ s }) => {
return (tree, file) => {
visit(tree, (node: Root | RootContent, index, parent) => {
if (node.type === 'element' && node.tagName === 'slot') {
if (typeof node.properties?.['is:inline'] !== 'undefined') return;
const name = node.properties?.['name'] ?? 'default';
const start = node.position?.start.offset ?? 0;
const end = node.position?.end.offset ?? 0;
const first = node.children.at(0) ?? node;
const last = node.children.at(-1) ?? node;
const text = file.value.slice(first.position?.start.offset ?? 0, last.position?.end.offset ?? 0).toString();
s.overwrite(start, end, `\${${SLOT_PREFIX}["${name}"] ?? \`${escape(text).trim()}\`}`)
}
});
}
}

export default rehypeSlots;

export const SLOT_PREFIX = `___SLOTS___`;
27 changes: 27 additions & 0 deletions packages/astro/src/vite-plugin-html/transform/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Element } from 'hast';
import MagicString from 'magic-string';

const splitAttrsTokenizer = /([\$\{\}\@a-z0-9_\:\-]*)\s*?=\s*?(['"]?)(.*?)\2\s+/gim;

export function replaceAttribute(s: MagicString, node: Element, key: string, newValue: string) {
splitAttrsTokenizer.lastIndex = 0;
const text = s.original.slice(node.position?.start.offset ?? 0, node.position?.end.offset ?? 0).toString();
const offset = text.indexOf(key);
if (offset === -1) return;
const start = node.position!.start.offset! + offset;
const tokens = text.slice(offset).split(splitAttrsTokenizer);
const token = tokens[0].replace(/([^>])(\>[\s\S]*$)/gmi, '$1');
if (token.trim() === key) {
const end = start + key.length;
s.overwrite(start, end, newValue)
} else {
const end = start + `${key}=${tokens[2]}${tokens[3]}${tokens[2]}`.length;
s.overwrite(start, end, newValue)
}
}
export function needsEscape(value: any): value is string {
return typeof value === 'string' && (value.includes('`') || value.includes('${'));
}
export function escape(value: string) {
return value.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
}
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/html-component/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/html-component",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>Hello component!</h1>

<div id="foo">bar</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import Test from '../components/Test.html';
---

<Test />
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/html-escape/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/html-escape",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>${foo}</div>
<span ${attr}></span>
<custom-element x-data="`${test}`"></custom-element>
<script>console.log(`hello ${"world"}!`)</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import Test from '../components/Test.html';
---

<Test />
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/html-page/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/html-page",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello page!</h1>
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/html-slots/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/html-slots",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="default"><slot></slot></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="inline"><slot is:inline></slot></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div id="a"><slot name="a"></slot></div>
<div id="b"><slot name="b"></slot></div>
<div id="c"><slot name="c"></slot></div>
13 changes: 13 additions & 0 deletions packages/astro/test/fixtures/html-slots/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import Default from '../components/Default.html';
import Named from '../components/Named.html';
import Inline from '../components/Inline.html';
---

<Default>Default</Default>
<Named>
<span slot="a">A</span>
<span slot="b">B</span>
<span slot="c">C</span>
</Named>
<Inline></Inline>
57 changes: 57 additions & 0 deletions packages/astro/test/html-component.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';

describe('HTML Component', () => {
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/html-component/',
});
});

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

it('works', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);

const h1 = $('h1');
const foo = $('#foo');

expect(h1.text()).to.equal('Hello component!');
expect(foo.text()).to.equal('bar');
});
});

describe('dev', () => {
let devServer;

before(async () => {
devServer = await fixture.startDevServer();
});

after(async () => {
await devServer.stop();
});

it('works', async () => {
const res = await fixture.fetch('/');

expect(res.status).to.equal(200);

const html = await res.text();
const $ = cheerio.load(html);

const h1 = $('h1');
const foo = $('#foo');

expect(h1.text()).to.equal('Hello component!');
expect(foo.text()).to.equal('bar');
});
});
});
Loading

0 comments on commit 7250e4e

Please sign in to comment.