Add support for doctype (#37)
* Add support for doctype * Automatically prepend doctype
This commit is contained in:
parent
dbd764bdeb
commit
3b27eaac43
22 changed files with 162 additions and 28 deletions
|
@ -7,6 +7,7 @@ import BaseLayout from '../components/BaseLayout.astro';
|
|||
export let content: any;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -7,6 +7,7 @@ import BaseLayout from '../components/BaseLayout.astro';
|
|||
export let content: any;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { format as formatDate, parseISO } from 'date-fns';
|
|||
export let content: any;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -6,6 +6,7 @@ let title = 'Not Found';
|
|||
let description = 'Snowpack is a lightning-fast frontend build tool, designed for the modern web.';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -40,6 +40,7 @@ let communityGuides;
|
|||
});
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -8,8 +8,8 @@ let title = 'Snowpack';
|
|||
let description = 'Snowpack is a lightning-fast frontend build tool, designed for the modern web.';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style lang="scss">
|
||||
@use '../../public/css/var' as *;
|
||||
|
@ -149,5 +149,4 @@ let description = 'Snowpack is a lightning-fast frontend build tool, designed fo
|
|||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async="async" defer="defer" src="https://buttons.github.io/buttons.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -16,6 +16,7 @@ const title = 'Community & News';
|
|||
const description = 'Snowpack community news and companies that use Snowpack.';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -7,6 +7,7 @@ let title = 'The Snowpack Plugin Catalog';
|
|||
let description = 'Snowpack plugins allow for configuration-minimal tooling integration.';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { TemplateNode } from '../parser/interfaces';
|
||||
|
||||
export type VisitorFn = (node: TemplateNode) => void;
|
||||
export type VisitorFn = (node: TemplateNode, parent: TemplateNode, type: string, index: number) => void;
|
||||
|
||||
export interface NodeVisitor {
|
||||
enter?: VisitorFn;
|
||||
|
|
33
src/compiler/optimize/doctype.ts
Normal file
33
src/compiler/optimize/doctype.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Optimizer } from '../../@types/optimizer';
|
||||
|
||||
export default function (_opts: { filename: string; fileID: string }): Optimizer {
|
||||
let hasDoctype = false;
|
||||
|
||||
return {
|
||||
visitors: {
|
||||
html: {
|
||||
Element: {
|
||||
enter(node, parent, _key, index) {
|
||||
if(node.name === '!doctype') {
|
||||
hasDoctype = true;
|
||||
}
|
||||
if(node.name === 'html' && !hasDoctype) {
|
||||
const dtNode = {
|
||||
start: 0, end: 0,
|
||||
attributes: [{ type: 'Attribute', name: 'html', value: true, start: 0, end: 0 }],
|
||||
children: [],
|
||||
name: '!doctype',
|
||||
type: 'Element'
|
||||
};
|
||||
parent.children!.splice(index, 0, dtNode);
|
||||
hasDoctype = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async finalize() {
|
||||
// Nothing happening here.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import { walk } from 'estree-walker';
|
||||
import type { Ast, TemplateNode } from '../../parser/interfaces';
|
||||
import { NodeVisitor, Optimizer, VisitorFn } from '../../@types/optimizer';
|
||||
|
||||
// Optimizers
|
||||
import optimizeStyles from './styles.js';
|
||||
import optimizeDoctype from './doctype.js';
|
||||
|
||||
interface VisitorCollection {
|
||||
enter: Map<string, VisitorFn[]>;
|
||||
|
@ -44,19 +47,19 @@ function createVisitorCollection() {
|
|||
|
||||
function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) {
|
||||
walk(tmpl, {
|
||||
enter(node) {
|
||||
enter(node, parent, key, index) {
|
||||
if (collection.enter.has(node.type)) {
|
||||
const fns = collection.enter.get(node.type)!;
|
||||
for (let fn of fns) {
|
||||
fn(node);
|
||||
fn(node, parent, key, index);
|
||||
}
|
||||
}
|
||||
},
|
||||
leave(node) {
|
||||
leave(node, parent, key, index) {
|
||||
if (collection.leave.has(node.type)) {
|
||||
const fns = collection.leave.get(node.type)!;
|
||||
for (let fn of fns) {
|
||||
fn(node);
|
||||
fn(node, parent, key, index);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -73,7 +76,7 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) {
|
|||
const cssVisitors = createVisitorCollection();
|
||||
const finalizers: Array<() => Promise<void>> = [];
|
||||
|
||||
const optimizers = [optimizeStyles(opts)];
|
||||
const optimizers = [optimizeStyles(opts), optimizeDoctype(opts)];
|
||||
|
||||
for (const optimizer of optimizers) {
|
||||
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
||||
|
|
|
@ -6,6 +6,15 @@ export type HTag = string | AstroComponent;
|
|||
const voidTags = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
||||
|
||||
function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
|
||||
if(tag === '!doctype') {
|
||||
yield '<!doctype ';
|
||||
if(attrs) {
|
||||
yield Object.keys(attrs).join(' ');
|
||||
}
|
||||
yield '>';
|
||||
return;
|
||||
}
|
||||
|
||||
yield `<${tag}`;
|
||||
if (attrs) {
|
||||
yield ' ';
|
||||
|
|
|
@ -2,12 +2,14 @@ import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadRes
|
|||
import type { AstroConfig } from './@types/astro';
|
||||
import type { LogOptions } from './logger';
|
||||
import type { CompileError } from './parser/utils/error.js';
|
||||
import { info, error, parseError } from './logger.js';
|
||||
import { info } from './logger.js';
|
||||
|
||||
import { existsSync, promises as fsPromises } from 'fs';
|
||||
import { loadConfiguration, startServer as startSnowpackServer } from 'snowpack';
|
||||
|
||||
const { readFile } = fsPromises;
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
loadConfiguration,
|
||||
logger as snowpackLogger,
|
||||
startServer as startSnowpackServer
|
||||
} from 'snowpack';
|
||||
|
||||
interface RuntimeConfig {
|
||||
astroConfig: AstroConfig;
|
||||
|
@ -27,6 +29,9 @@ type LoadResultError = { statusCode: 500 } & ({ type: 'parse-error'; error: Comp
|
|||
|
||||
export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultError;
|
||||
|
||||
// Disable snowpack from writing to stdout/err.
|
||||
snowpackLogger.level = 'silent';
|
||||
|
||||
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
||||
const { logging, snowpack, snowpackRuntime } = config;
|
||||
const { astroRoot } = config.astroConfig;
|
||||
|
@ -130,13 +135,18 @@ export async function createRuntime(astroConfig: AstroConfig, { logging }: Runti
|
|||
resolve: async (pkgName: string) => snowpack.getUrlForPackage(pkgName),
|
||||
};
|
||||
|
||||
const mountOptions = {
|
||||
[astroRoot.pathname]: '/_astro',
|
||||
[internalPath.pathname]: '/_astro_internal'
|
||||
}
|
||||
|
||||
if(existsSync(astroConfig.public)) {
|
||||
mountOptions[astroConfig.public.pathname] = '/';
|
||||
}
|
||||
|
||||
const snowpackConfig = await loadConfiguration({
|
||||
root: projectRoot.pathname,
|
||||
mount: {
|
||||
[astroRoot.pathname]: '/_astro',
|
||||
[internalPath.pathname]: '/_astro_internal',
|
||||
public: '/',
|
||||
},
|
||||
mount: mountOptions,
|
||||
plugins: [[new URL('../snowpack-plugin.cjs', import.meta.url).pathname, astroPlugOptions], '@snowpack/plugin-sass', '@snowpack/plugin-svelte', '@snowpack/plugin-vue'],
|
||||
devOptions: {
|
||||
open: 'none',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { createRuntime } from '../lib/runtime.js';
|
||||
import { loadConfig } from '../lib/config.js';
|
||||
import { doc } from './test-utils.js';
|
||||
|
||||
const Basics = suite('HMX Basics');
|
||||
|
@ -8,18 +9,14 @@ const Basics = suite('HMX Basics');
|
|||
let runtime;
|
||||
|
||||
Basics.before(async () => {
|
||||
const astroConfig = {
|
||||
projectRoot: new URL('./fixtures/astro-basic/', import.meta.url),
|
||||
hmxRoot: new URL('./fixtures/astro-basic/astro/', import.meta.url),
|
||||
dist: './_site',
|
||||
};
|
||||
const astroConfig = await loadConfig(new URL('./fixtures/astro-basics', import.meta.url).pathname);
|
||||
|
||||
const logging = {
|
||||
level: 'error',
|
||||
dest: process.stderr,
|
||||
};
|
||||
|
||||
runtime = await createRuntime(astroConfig, logging);
|
||||
runtime = await createRuntime(astroConfig, {logging});
|
||||
});
|
||||
|
||||
Basics.after(async () => {
|
||||
|
|
54
test/astro-doctype.test.js
Normal file
54
test/astro-doctype.test.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { loadConfig } from '../lib/config.js';
|
||||
import { createRuntime } from '../lib/runtime.js';
|
||||
|
||||
const DType = suite('doctype');
|
||||
|
||||
let runtime, setupError;
|
||||
|
||||
DType.before(async () => {
|
||||
try {
|
||||
const astroConfig = await loadConfig(new URL('./fixtures/astro-doctype', import.meta.url).pathname);
|
||||
|
||||
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');
|
||||
|
||||
assert.equal(result.statusCode, 200);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
||||
});
|
||||
|
||||
DType.skip('Preserves user provided doctype', async () => {
|
||||
const result = await runtime.load('/preserve');
|
||||
|
||||
assert.equal(result.statusCode, 200);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
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.run();
|
|
@ -17,7 +17,7 @@ Markdown.before(async () => {
|
|||
};
|
||||
|
||||
try {
|
||||
runtime = await createRuntime(astroConfig, logging);
|
||||
runtime = await createRuntime(astroConfig, {logging});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setupError = err;
|
||||
|
|
|
@ -16,7 +16,7 @@ StylesSSR.before(async () => {
|
|||
dest: process.stderr,
|
||||
};
|
||||
|
||||
runtime = await createRuntime(astroConfig, logging);
|
||||
runtime = await createRuntime(astroConfig, {logging});
|
||||
});
|
||||
|
||||
StylesSSR.after(async () => {
|
||||
|
|
5
test/fixtures/astro-doctype/astro.config.mjs
vendored
Normal file
5
test/fixtures/astro-doctype/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
projectRoot: '.',
|
||||
astroRoot: './astro',
|
||||
dist: './_site',
|
||||
};
|
8
test/fixtures/astro-doctype/astro/pages/prepend.astro
vendored
Normal file
8
test/fixtures/astro-doctype/astro/pages/prepend.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
let title = 'My Site';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head><title>{title}</title></head>
|
||||
<body><h1>Hello world</h1></body>
|
||||
</html>
|
9
test/fixtures/astro-doctype/astro/pages/preserve.astro
vendored
Normal file
9
test/fixtures/astro-doctype/astro/pages/preserve.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
let title = 'My Site';
|
||||
---
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html lang="en">
|
||||
<head><title>{title}</title></head>
|
||||
<body><h1>Hello world</h1></body>
|
||||
</html>
|
|
@ -17,7 +17,7 @@ React.before(async () => {
|
|||
};
|
||||
|
||||
try {
|
||||
runtime = await createRuntime(astroConfig, logging);
|
||||
runtime = await createRuntime(astroConfig, {logging});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setupError = err;
|
||||
|
|
|
@ -25,7 +25,7 @@ SnowpackDev.before(async () => {
|
|||
};
|
||||
|
||||
try {
|
||||
runtime = await createRuntime(astroConfig, logging);
|
||||
runtime = await createRuntime(astroConfig, {logging});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setupError = err;
|
||||
|
|
Loading…
Reference in a new issue