summaryrefslogtreecommitdiff
path: root/packages/astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src')
-rw-r--r--packages/astro/src/@types/astro.ts13
-rw-r--r--packages/astro/src/@types/compiler.ts3
-rw-r--r--packages/astro/src/@types/hydrate.ts1
-rw-r--r--packages/astro/src/@types/resolve.d.ts1
-rw-r--r--packages/astro/src/build.ts13
-rw-r--r--packages/astro/src/build/page.ts260
-rw-r--r--packages/astro/src/compiler/codegen/index.ts230
-rw-r--r--packages/astro/src/compiler/index.ts3
-rw-r--r--packages/astro/src/compiler/transform/hydration.ts63
-rw-r--r--packages/astro/src/compiler/transform/index.ts3
-rw-r--r--packages/astro/src/compiler/utils.ts5
-rw-r--r--packages/astro/src/frontend/SvelteWrapper.svelte7
-rw-r--r--packages/astro/src/frontend/__astro_component.ts100
-rw-r--r--packages/astro/src/frontend/hydrate/idle.ts23
-rw-r--r--packages/astro/src/frontend/hydrate/load.ts14
-rw-r--r--packages/astro/src/frontend/hydrate/visible.ts32
-rw-r--r--packages/astro/src/frontend/render/preact.ts31
-rw-r--r--packages/astro/src/frontend/render/react.ts32
-rw-r--r--packages/astro/src/frontend/render/renderer.ts62
-rw-r--r--packages/astro/src/frontend/render/svelte.ts27
-rw-r--r--packages/astro/src/frontend/render/utils.ts55
-rw-r--r--packages/astro/src/frontend/render/vue.ts65
-rw-r--r--packages/astro/src/frontend/renderer-astro.ts8
-rw-r--r--packages/astro/src/frontend/runtime/svelte.ts10
-rw-r--r--packages/astro/src/runtime.ts75
25 files changed, 384 insertions, 752 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 2f9983f53..ddd08b726 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1,3 +1,5 @@
+import type { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from '@babel/types';
+
export interface AstroConfigRaw {
dist: string;
projectRoot: string;
@@ -6,8 +8,6 @@ export interface AstroConfigRaw {
jsx?: string;
}
-export type ValidExtensionPlugins = 'astro' | 'react' | 'preact' | 'svelte' | 'vue';
-
export interface AstroMarkdownOptions {
/** Enable or disable footnotes syntax extension */
footnotes: boolean;
@@ -19,7 +19,7 @@ export interface AstroConfig {
projectRoot: URL;
astroRoot: URL;
public: URL;
- extensions?: Record<string, ValidExtensionPlugins>;
+ renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: Partial<AstroMarkdownOptions>;
/** Options specific to `astro build` */
@@ -171,3 +171,10 @@ export interface CollectionResult<T = any> {
/** Matched parameters, if any */
params: Params;
}
+
+export interface ComponentInfo {
+ url: string;
+ importSpecifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier;
+}
+
+export type Components = Map<string, ComponentInfo>;
diff --git a/packages/astro/src/@types/compiler.ts b/packages/astro/src/@types/compiler.ts
index a661f1622..10703c523 100644
--- a/packages/astro/src/@types/compiler.ts
+++ b/packages/astro/src/@types/compiler.ts
@@ -1,11 +1,10 @@
import type { LogOptions } from '../logger';
-import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from './astro';
+import type { AstroConfig, RuntimeMode } from './astro';
export interface CompileOptions {
logging: LogOptions;
resolvePackageUrl: (p: string) => Promise<string>;
astroConfig: AstroConfig;
- extensions?: Record<string, ValidExtensionPlugins>;
mode: RuntimeMode;
tailwindConfig?: string;
}
diff --git a/packages/astro/src/@types/hydrate.ts b/packages/astro/src/@types/hydrate.ts
new file mode 100644
index 000000000..1f89b6464
--- /dev/null
+++ b/packages/astro/src/@types/hydrate.ts
@@ -0,0 +1 @@
+export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string|null) => void>;
diff --git a/packages/astro/src/@types/resolve.d.ts b/packages/astro/src/@types/resolve.d.ts
new file mode 100644
index 000000000..a4cc7d062
--- /dev/null
+++ b/packages/astro/src/@types/resolve.d.ts
@@ -0,0 +1 @@
+declare module 'resolve';
diff --git a/packages/astro/src/build.ts b/packages/astro/src/build.ts
index be9ec8c7b..208072390 100644
--- a/packages/astro/src/build.ts
+++ b/packages/astro/src/build.ts
@@ -6,6 +6,7 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { performance } from 'perf_hooks';
+import eslexer from 'es-module-lexer';
import cheerio from 'cheerio';
import del from 'del';
import { bold, green, yellow } from 'kleur/colors';
@@ -94,6 +95,8 @@ export async function build(astroConfig: AstroConfig, logging: LogOptions = defa
// after pages are built, build depTree
timer.deps = performance.now();
const scanPromises: Promise<void>[] = [];
+
+ await eslexer.init;
for (const id of Object.keys(buildState)) {
if (buildState[id].contentType !== 'text/html') continue; // only scan HTML files
const pageDeps = findDeps(buildState[id].contents as string, {
@@ -237,8 +240,16 @@ export function findDeps(html: string, { astroConfig, srcPath }: { astroConfig:
$('script').each((i, el) => {
const src = $(el).attr('src');
- if (src && !isRemote(src)) {
+ if (src) {
+ if (isRemote(src)) return;
pageDeps.js.add(getDistPath(src, { astroConfig, srcPath }));
+ } else {
+ const text = $(el).html();
+ if (!text) return;
+ const [imports] = eslexer.parse(text);
+ for (const spec of imports) {
+ if (spec.n) pageDeps.js.add(spec.n);
+ }
}
});
diff --git a/packages/astro/src/build/page.ts b/packages/astro/src/build/page.ts
index a83a945d3..b53b5f5bc 100644
--- a/packages/astro/src/build/page.ts
+++ b/packages/astro/src/build/page.ts
@@ -1,22 +1,8 @@
-import type { ImportDeclaration } from '@babel/types';
-import type { AstroConfig, BuildOutput, RuntimeMode, ValidExtensionPlugins } from '../@types/astro';
+import type { AstroConfig, BuildOutput, RuntimeMode } from '../@types/astro';
import type { AstroRuntime, LoadResult } from '../runtime';
import type { LogOptions } from '../logger';
-
-import fs from 'fs';
import path from 'path';
-import mime from 'mime';
-import { fileURLToPath } from 'url';
-import babelParser from '@babel/parser';
-import { parse } from 'astro-parser';
-import esbuild from 'esbuild';
-import { walk } from 'estree-walker';
import { generateRSS } from './rss.js';
-import { getAttrValue } from '../ast.js';
-import { convertMdToAstroSource } from '../compiler/index.js';
-import { transform } from '../compiler/transform/index.js';
-
-type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>;
interface PageBuildOptions {
astroConfig: AstroConfig;
@@ -62,7 +48,6 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode
const [result] = await Promise.all([
loadCollection(outURL) as Promise<LoadResult>, // first run will always return a result so assert type here
- gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }),
]);
if (result.statusCode >= 500) {
@@ -103,11 +88,11 @@ export async function buildCollectionPage({ astroConfig, filepath, logging, mode
}
/** Build static page */
-export async function buildStaticPage({ astroConfig, buildState, filepath, logging, mode, resolvePackageUrl, runtime }: PageBuildOptions): Promise<void> {
+export async function buildStaticPage({ astroConfig, buildState, filepath, runtime }: PageBuildOptions): Promise<void> {
const pagesPath = new URL('./pages/', astroConfig.astroRoot);
const url = filepath.pathname.replace(pagesPath.pathname, '/').replace(/(index)?\.(astro|md)$/, '');
- // build page in parallel with gathering runtimes
+ // build page in parallel
await Promise.all([
runtime.load(url).then((result) => {
if (result.statusCode !== 200) throw new Error((result as any).error);
@@ -118,243 +103,6 @@ export async function buildStaticPage({ astroConfig, buildState, filepath, loggi
contentType: 'text/html',
encoding: 'utf8',
};
- }),
- gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }),
- ]);
-}
-
-/** Evaluate mustache expression (safely) */
-function compileExpressionSafe(raw: string): string {
- let { code } = esbuild.transformSync(raw, {
- loader: 'tsx',
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- charset: 'utf8',
- });
- return code;
-}
-
-/** Add framework runtimes when needed */
-async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
- const importMap: DynamicImportMap = new Map();
- for (let plugin of plugins) {
- switch (plugin) {
- case 'svelte': {
- importMap.set('svelte', await resolvePackageUrl('svelte'));
- break;
- }
- case 'vue': {
- importMap.set('vue', await resolvePackageUrl('vue'));
- break;
- }
- case 'react': {
- importMap.set('react', await resolvePackageUrl('react'));
- importMap.set('react-dom', await resolvePackageUrl('react-dom'));
- break;
- }
- case 'preact': {
- importMap.set('preact', await resolvePackageUrl('preact'));
- break;
- }
- }
- }
- return importMap;
-}
-
-const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
- '.jsx': 'react',
- '.tsx': 'react',
- '.svelte': 'svelte',
- '.vue': 'vue',
-};
-
-/** Gather necessary framework runtimes (React, Vue, Svelte, etc.) for dynamic components */
-async function gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }: PageBuildOptions): Promise<Set<string>> {
- const imports = new Set<string>();
-
- // Only astro files
- if (!filepath.pathname.endsWith('.astro') && !filepath.pathname.endsWith('.md')) {
- return imports;
- }
-
- const extensions = astroConfig.extensions || defaultExtensions;
-
- let source = await fs.promises.readFile(filepath, 'utf8');
- if (filepath.pathname.endsWith('.md')) {
- source = await convertMdToAstroSource(source, { filename: fileURLToPath(filepath) });
- }
-
- const ast = parse(source, { filepath });
-
- if (!ast.module) {
- return imports;
- }
-
- await transform(ast, {
- filename: fileURLToPath(filepath),
- fileID: '',
- compileOptions: {
- astroConfig,
- resolvePackageUrl,
- logging,
- mode,
- },
- });
-
- const componentImports: ImportDeclaration[] = [];
- const components: Record<string, { plugin: ValidExtensionPlugins; type: string; specifier: string }> = {};
- const plugins = new Set<ValidExtensionPlugins>();
-
- const program = babelParser.parse(ast.module.content, {
- sourceType: 'module',
- plugins: ['jsx', 'typescript', 'topLevelAwait'],
- }).program;
-
- const { body } = program;
- let i = body.length;
- while (--i >= 0) {
- const node = body[i];
- if (node.type === 'ImportDeclaration') {
- componentImports.push(node);
- }
- }
-
- for (const componentImport of componentImports) {
- const importUrl = componentImport.source.value;
- const componentType = path.posix.extname(importUrl);
- for (const specifier of componentImport.specifiers) {
- if (specifier.type === 'ImportDefaultSpecifier') {
- const componentName = specifier.local.name;
- const plugin = extensions[componentType] || defaultExtensions[componentType];
- plugins.add(plugin);
- components[componentName] = {
- plugin,
- type: componentType,
- specifier: importUrl,
- };
- break;
- }
- }
- }
-
- const dynamic = await acquireDynamicComponentImports(plugins, resolvePackageUrl);
-
- /** Add dynamic component runtimes to imports */
- function appendImports(rawName: string, importUrl: URL) {
- const [componentName, componentType] = rawName.split(':');
- if (!componentType) {
- return;
- }
-
- if (!components[componentName]) {
- throw new Error(`Unknown Component: ${componentName}`);
- }
-
- const defn = components[componentName];
- const fileUrl = new URL(defn.specifier, importUrl);
- let rel = path.posix.relative(astroConfig.astroRoot.pathname, fileUrl.pathname);
-
- switch (defn.plugin) {
- case 'preact': {
- const preact = dynamic.get('preact');
- if (!preact) throw new Error(`Unable to load Preact plugin`);
- imports.add(preact);
- rel = rel.replace(/\.[^.]+$/, '.js');
- break;
- }
- case 'react': {
- const [react, reactDOM] = [dynamic.get('react'), dynamic.get('react-dom')];
- if (!react || !reactDOM) throw new Error(`Unable to load React plugin`);
- imports.add(react);
- imports.add(reactDOM);
- rel = rel.replace(/\.[^.]+$/, '.js');
- break;
- }
- case 'vue': {
- const vue = dynamic.get('vue');
- if (!vue) throw new Error('Unable to load Vue plugin');
- imports.add(vue);
- rel = rel.replace(/\.[^.]+$/, '.vue.js');
- break;
- }
- case 'svelte': {
- const svelte = dynamic.get('svelte');
- if (!svelte) throw new Error('Unable to load Svelte plugin');
- imports.add(svelte);
- imports.add('/_astro_internal/runtime/svelte.js');
- rel = rel.replace(/\.[^.]+$/, '.svelte.js');
- break;
- }
- }
-
- imports.add(`/_astro/${rel}`);
- }
-
- walk(ast.html, {
- enter(node) {
- switch (node.type) {
- case 'Element': {
- if (node.name !== 'script') return;
- if (getAttrValue(node.attributes, 'type') !== 'module') return;
-
- const src = getAttrValue(node.attributes, 'src');
-
- if (src && src.startsWith('/')) {
- imports.add(src);
- }
- break;
- }
-
- case 'MustacheTag': {
- let code: string;
- try {
- code = compileExpressionSafe(node.content);
- } catch {
- return;
- }
-
- let matches: RegExpExecArray[] = [];
- let match: RegExpExecArray | null | undefined;
- const H_COMPONENT_SCANNER = /h\(['"]?([A-Z].*?)['"]?,/gs;
- const regex = new RegExp(H_COMPONENT_SCANNER);
- while ((match = regex.exec(code))) {
- matches.push(match);
- }
- for (const foundImport of matches.reverse()) {
- const name = foundImport[1];
- appendImports(name, filepath);
- }
- break;
- }
- case 'InlineComponent': {
- if (/^[A-Z]/.test(node.name)) {
- appendImports(node.name, filepath);
- return;
- }
-
- break;
- }
- }
- },
- });
-
- // add all imports to build output
- await Promise.all(
- [...imports].map(async (url) => {
- if (buildState[url]) return; // don’t build already-built URLs
-
- // add new results to buildState
- const result = await runtime.load(url);
- if (result.statusCode === 200) {
- buildState[url] = {
- srcPath: filepath,
- contents: result.contents,
- contentType: result.contentType || mime.getType(url) || '',
- encoding: 'utf8',
- };
- }
})
- );
-
- return imports;
+ ]);
}
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index 8535681ea..25243a79c 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -1,6 +1,7 @@
import type { Ast, Script, Style, TemplateNode } from 'astro-parser';
import type { CompileOptions } from '../../@types/compiler';
-import type { AstroConfig, AstroMarkdownOptions, TransformResult, ValidExtensionPlugins } from '../../@types/astro';
+import type { AstroConfig, AstroMarkdownOptions, TransformResult, ComponentInfo, Components } from '../../@types/astro';
+import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types';
import 'source-map-support/register.js';
import eslexer from 'es-module-lexer';
@@ -12,12 +13,11 @@ import _babelGenerator from '@babel/generator';
import babelParser from '@babel/parser';
import { codeFrameColumns } from '@babel/code-frame';
import * as babelTraverse from '@babel/traverse';
-import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types';
import { error, warn } from '../../logger.js';
import { fetchContent } from './content.js';
import { isFetchContent } from './utils.js';
import { yellow } from 'kleur/colors';
-import { MarkdownRenderingOptions, renderMarkdown } from '../utils';
+import { isComponentTag, renderMarkdown } from '../utils';
const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
@@ -126,134 +126,44 @@ function generateAttributes(attrs: Record<string, string>): string {
return result + '}';
}
-interface ComponentInfo {
- type: string;
- url: string;
- plugin: string | undefined;
-}
-
-const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
- '.astro': 'astro',
- '.jsx': 'react',
- '.tsx': 'react',
- '.vue': 'vue',
- '.svelte': 'svelte',
-};
-
-type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>;
-
interface GetComponentWrapperOptions {
filename: string;
astroConfig: AstroConfig;
- dynamicImports: DynamicImportMap;
}
+const PlainExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
/** Generate Astro-friendly component import */
-function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, opts: GetComponentWrapperOptions) {
- const { astroConfig, dynamicImports, filename } = opts;
+function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) {
+ const { astroConfig, filename } = opts;
const { astroRoot } = astroConfig;
- const [name, kind] = _name.split(':');
const currFileUrl = new URL(`file://${filename}`);
-
- if (!plugin) {
- throw new Error(`No supported plugin found for ${type ? `extension ${type}` : `${url} (try adding an extension)`}`);
- }
-
- const getComponentUrl = (ext = '.js') => {
+ const [name, kind] = _name.split(':');
+ const getComponentUrl = () => {
+ const componentExt = path.extname(url);
+ const ext = PlainExtensions.has(componentExt) ? '.js' : `${componentExt}.js`;
const outUrl = new URL(url, currFileUrl);
return '/_astro/' + path.posix.relative(astroRoot.pathname, outUrl.pathname).replace(/\.[^.]+$/, ext);
};
-
- switch (plugin) {
- case 'astro': {
- if (kind) {
- throw new Error(`Astro does not support :${kind}`);
- }
- return {
- wrapper: name,
- wrapperImport: ``,
- };
- }
- case 'preact': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__preact_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl(),
- componentExport: 'default',
- frameworkUrls: {
- preact: dynamicImports.get('preact'),
- },
- })})`,
- wrapperImport: `import {__preact_${kind}} from '${internalImport('render/preact.js')}';`,
- };
- }
-
- return {
- wrapper: `__preact_static(${name})`,
- wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`,
- };
- }
- case 'react': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__react_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl(),
- componentExport: 'default',
- frameworkUrls: {
- react: dynamicImports.get('react'),
- 'react-dom': dynamicImports.get('react-dom'),
- },
- })})`,
- wrapperImport: `import {__react_${kind}} from '${internalImport('render/react.js')}';`,
- };
+ const getComponentExport = () => {
+ switch (importSpecifier.type) {
+ case 'ImportDefaultSpecifier': return { value: 'default' };
+ case 'ImportSpecifier': {
+ if (importSpecifier.imported.type === 'Identifier') {
+ return { value: importSpecifier.imported.name };
+ }
+ return { value: importSpecifier.imported.value };
}
-
- return {
- wrapper: `__react_static(${name})`,
- wrapperImport: `import {__react_static} from '${internalImport('render/react.js')}';`,
- };
- }
- case 'svelte': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__svelte_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl('.svelte.js'),
- componentExport: 'default',
- frameworkUrls: {
- 'svelte-runtime': internalImport('runtime/svelte.js'),
- },
- })})`,
- wrapperImport: `import {__svelte_${kind}} from '${internalImport('render/svelte.js')}';`,
- };
+ case 'ImportNamespaceSpecifier': {
+ const [_, value] = name.split('.');
+ return { value };
}
-
- return {
- wrapper: `__svelte_static(${name})`,
- wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';`,
- };
}
- case 'vue': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__vue_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl('.vue.js'),
- componentExport: 'default',
- frameworkUrls: {
- vue: dynamicImports.get('vue'),
- },
- })})`,
- wrapperImport: `import {__vue_${kind}} from '${internalImport('render/vue.js')}';`,
- };
- }
+ }
- return {
- wrapper: `__vue_static(${name})`,
- wrapperImport: `import {__vue_static} from '${internalImport('render/vue.js')}';`,
- };
- }
- default: {
- throw new Error(`Unknown component type`);
- }
+ const importInfo = kind ? { componentUrl: getComponentUrl(), componentExport: getComponentExport() } : {};
+ return {
+ wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: kind, displayName: name, ...importInfo })})`,
+ wrapperImport: `import {__astro_component} from '${internalImport('__astro_component.js')}';`,
}
}
@@ -268,38 +178,8 @@ function compileExpressionSafe(raw: string): string {
return code;
}
-/** Build dependency map of dynamic component runtime frameworks */
-async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
- const importMap: DynamicImportMap = new Map();
- for (let plugin of plugins) {
- switch (plugin) {
- case 'vue': {
- importMap.set('vue', await resolvePackageUrl('vue'));
- break;
- }
- case 'react': {
- importMap.set('react', await resolvePackageUrl('react'));
- importMap.set('react-dom', await resolvePackageUrl('react-dom'));
- break;
- }
- case 'preact': {
- importMap.set('preact', await resolvePackageUrl('preact'));
- break;
- }
- case 'svelte': {
- importMap.set('svelte', await resolvePackageUrl('svelte'));
- break;
- }
- }
- }
- return importMap;
-}
-
-type Components = Record<string, { type: string; url: string; plugin: string | undefined }>;
-
interface CompileResult {
script: string;
- componentPlugins: Set<ValidExtensionPlugins>;
createCollection?: string;
}
@@ -311,25 +191,20 @@ interface CodegenState {
insideMarkdown: boolean | Record<string, any>;
};
importExportStatements: Set<string>;
- dynamicImports: DynamicImportMap;
}
/** Compile/prepare Astro frontmatter scripts */
function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions): CompileResult {
- const { extensions = defaultExtensions } = compileOptions;
-
const componentImports: ImportDeclaration[] = [];
const componentProps: VariableDeclarator[] = [];
const componentExports: ExportNamedDeclaration[] = [];
const contentImports = new Map<string, { spec: string; declarator: string }>();
- const importSpecifierTypes = new Set(['ImportDefaultSpecifier', 'ImportSpecifier']);
let script = '';
let propsStatement = '';
let contentCode = ''; // code for handling Astro.fetchContent(), if any;
let createCollection = ''; // function for executing collection
- const componentPlugins = new Set<ValidExtensionPlugins>();
if (module) {
const parseOptions: babelParser.ParserOptions = {
@@ -420,19 +295,12 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
for (const componentImport of componentImports) {
const importUrl = componentImport.source.value;
- const componentType = path.posix.extname(importUrl);
- const specifier = componentImport.specifiers[0];
- if (!specifier) continue; // this is unused
- // set componentName to default import if used (user), or use filename if no default import (mostly internal use)
- const componentName = importSpecifierTypes.has(specifier.type) ? specifier.local.name : path.posix.basename(importUrl, componentType);
- const plugin = extensions[componentType] || defaultExtensions[componentType];
- state.components[componentName] = {
- type: componentType,
- plugin,
- url: importUrl,
- };
- if (plugin) {
- componentPlugins.add(plugin);
+ for (const specifier of componentImport.specifiers) {
+ const componentName = specifier.local.name;
+ state.components.set(componentName, {
+ importSpecifier: specifier,
+ url: importUrl,
+ });
}
const { start, end } = componentImport;
state.importExportStatements.add(module.content.slice(start || undefined, end || undefined));
@@ -518,7 +386,6 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
return {
script,
- componentPlugins,
createCollection: createCollection || undefined,
};
}
@@ -550,7 +417,7 @@ function dedent(str: string) {
/** Compile page markup */
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
return new Promise((resolve) => {
- const { components, css, importExportStatements, dynamicImports, filename } = state;
+ const { components, css, importExportStatements, filename } = state;
const { astroConfig } = compileOptions;
let paren = -1;
@@ -625,8 +492,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
paren++;
return;
}
- const COMPONENT_NAME_SCANNER = /^[A-Z]/;
- if (!COMPONENT_NAME_SCANNER.test(name)) {
+ if (!isComponentTag(name)) {
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
@@ -635,19 +501,21 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
return;
}
const [componentName, componentKind] = name.split(':');
- const componentImportData = components[componentName];
- if (!componentImportData) {
+ let componentInfo = components.get(componentName);
+ if (/\./.test(componentName)) {
+ const [componentNamespace] = componentName.split('.');
+ componentInfo = components.get(componentNamespace);
+ }
+ if (!componentInfo) {
throw new Error(`Unknown Component: ${componentName}`);
}
- if (componentImportData.type === '.astro') {
- if (componentName === 'Markdown') {
- const { $scope } = attributes ?? {};
- state.markers.insideMarkdown = { $scope };
- curr = 'markdown';
- return;
- }
+ if (componentName === 'Markdown') {
+ const { $scope } = attributes ?? {};
+ state.markers.insideMarkdown = { $scope };
+ curr = 'markdown';
+ return;
}
- const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
+ const { wrapper, wrapperImport } = getComponentWrapper(name, componentInfo, { astroConfig, filename });
if (wrapperImport) {
importExportStatements.add(wrapperImport);
}
@@ -762,17 +630,15 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
const state: CodegenState = {
filename,
- components: {},
+ components: new Map(),
css: [],
markers: {
insideMarkdown: false,
},
importExportStatements: new Set(),
- dynamicImports: new Map(),
};
- const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions);
- state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolvePackageUrl);
+ const { script, createCollection } = compileModule(ast.module, state, compileOptions);
compileCss(ast.css, state);
diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts
index a03e9c2ae..ac107ae7a 100644
--- a/packages/astro/src/compiler/index.ts
+++ b/packages/astro/src/compiler/index.ts
@@ -130,7 +130,7 @@ async function __render(props, ...children) {
${result.script}
return h(Fragment, null, ${result.html});
}
-export default __render;
+export default { isAstroComponent: true, __render };
${result.createCollection || ''}
@@ -138,6 +138,7 @@ ${result.createCollection || ''}
// triggered by loading a component directly by URL.
export async function __renderPage({request, children, props}) {
const currentChild = {
+ isAstroComponent: true,
layout: typeof __layout === 'undefined' ? undefined : __layout,
content: typeof __content === 'undefined' ? undefined : __content,
__render,
diff --git a/packages/astro/src/compiler/transform/hydration.ts b/packages/astro/src/compiler/transform/hydration.ts
new file mode 100644
index 000000000..23b0fd4ba
--- /dev/null
+++ b/packages/astro/src/compiler/transform/hydration.ts
@@ -0,0 +1,63 @@
+import { Transformer } from '../../@types/transformer';
+import type { TemplateNode } from 'astro-parser';
+
+/** If there are hydrated components, inject styles for [data-astro-root] and [data-astro-children] */
+export default function (): Transformer {
+ let head: TemplateNode;
+ let body: TemplateNode;
+ let hasComponents = false;
+
+ return {
+ visitors: {
+ html: {
+ InlineComponent: {
+ enter(node, parent) {
+ const [name, kind] = node.name.split(':');
+ if (kind && !hasComponents) {
+ hasComponents = true;
+ }
+ }
+ },
+ Element: {
+ enter(node) {
+ if (!hasComponents) return;
+ switch (node.name) {
+ case 'head': {
+ head = node;
+ return;
+ }
+ case 'body': {
+ body = node;
+ return;
+ }
+ default: return;
+ }
+ }
+ }
+ },
+ },
+ async finalize() {
+ if (!(head && hasComponents)) return;
+
+ const style: TemplateNode = {
+ type: 'Element',
+ name: 'style',
+ attributes: [
+ { name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] },
+ ],
+ start: 0,
+ end: 0,
+ children: [
+ {
+ start: 0,
+ end: 0,
+ type: 'Text',
+ data: 'astro-root, astro-fragment { display: contents; }',
+ raw: 'astro-root, astro-fragment { display: contents; }'
+ }
+ ]
+ };
+ head.children = [...(head.children ?? []), style];
+ },
+ };
+}
diff --git a/packages/astro/src/compiler/transform/index.ts b/packages/astro/src/compiler/transform/index.ts
index 27f5a3212..d622846d9 100644
--- a/packages/astro/src/compiler/transform/index.ts
+++ b/packages/astro/src/compiler/transform/index.ts
@@ -8,6 +8,7 @@ import transformStyles from './styles.js';
import transformDoctype from './doctype.js';
import transformModuleScripts from './module-scripts.js';
import transformCodeBlocks from './prism.js';
+import transformHydration from './hydration.js';
interface VisitorCollection {
enter: Map<string, VisitorFn[]>;
@@ -84,7 +85,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = [];
- const optimizers = [transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
+ const optimizers = [transformHydration(), transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
for (const optimizer of optimizers) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
diff --git a/packages/astro/src/compiler/utils.ts b/packages/astro/src/compiler/utils.ts
index 5275a42a7..2b7438d87 100644
--- a/packages/astro/src/compiler/utils.ts
+++ b/packages/astro/src/compiler/utils.ts
@@ -66,3 +66,8 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
content: result.toString(),
};
}
+
+/** Is the given string a valid component tag */
+export function isComponentTag(tag: string) {
+ return /^[A-Z]/.test(tag) || /^[a-z]+\./.test(tag);
+}
diff --git a/packages/astro/src/frontend/SvelteWrapper.svelte b/packages/astro/src/frontend/SvelteWrapper.svelte
deleted file mode 100644
index c3e926128..000000000
--- a/packages/astro/src/frontend/SvelteWrapper.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-<script>
-const { __astro_component: Component, __astro_children, ...props } = $$props;
-</script>
-
-<svelte:component this={Component} {...props}>
- {@html __astro_children}
-</svelte:component>
diff --git a/packages/astro/src/frontend/__astro_component.ts b/packages/astro/src/frontend/__astro_component.ts
new file mode 100644
index 000000000..dca4e45f2
--- /dev/null
+++ b/packages/astro/src/frontend/__astro_component.ts
@@ -0,0 +1,100 @@
+import hash from 'shorthash';
+import { valueToEstree, Value } from 'estree-util-value-to-estree';
+import { generate } from 'astring';
+import * as astro from './renderer-astro';
+
+// A more robust version alternative to `JSON.stringify` that can handle most values
+// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
+const serialize = (value: Value) => generate(valueToEstree(value));
+
+/**
+ * These values are dynamically injected by Snowpack.
+ * See comment in `snowpack-plugin.cjs`!
+ *
+ * In a world where Snowpack supports virtual files, this won't be necessary.
+ * It would ideally look something like:
+ *
+ * ```ts
+ * import { __rendererSources, __renderers } from "virtual:astro/runtime"
+ * ```
+ */
+declare let __rendererSources: string[];
+declare let __renderers: any[];
+
+__rendererSources = ['', ...__rendererSources];
+__renderers = [astro, ...__renderers];
+
+const rendererCache = new WeakMap();
+
+/** For a given component, resolve the renderer. Results are cached if this instance is encountered again */
+function resolveRenderer(Component: any, props: any = {}) {
+ if (rendererCache.has(Component)) {
+ return rendererCache.get(Component);
+ }
+
+ for (const __renderer of __renderers) {
+ const shouldUse = __renderer.check(Component, props)
+ if (shouldUse) {
+ rendererCache.set(Component, __renderer);
+ return __renderer;
+ }
+ }
+}
+
+interface AstroComponentProps {
+ displayName: string;
+ hydrate?: 'load'|'idle'|'visible';
+ componentUrl?: string;
+ componentExport?: { value: string, namespace?: boolean };
+}
+
+
+/** For hydrated components, generate a <script type="module"> to load the component */
+async function generateHydrateScript({ renderer, astroId, props }: any, { hydrate, componentUrl, componentExport }: Required<AstroComponentProps>) {
+ const rendererSource = __rendererSources[__renderers.findIndex(r => r === renderer)];
+
+ const script = `<script type="module">
+import setup from '/_astro_internal/hydrate/${hydrate}.js';
+setup("${astroId}", async () => {
+ const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${rendererSource}")]);
+ return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
+});
+</script>`;
+
+ return script;
+}
+
+
+export const __astro_component = (Component: any, componentProps: AstroComponentProps = {} as any) => {
+ if (Component == null) {
+ throw new Error(`Unable to render <${componentProps.displayName}> because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
+ }
+ // First attempt at resolving a renderer (we don't have the props yet, so it might fail if they are required)
+ let renderer = resolveRenderer(Component);
+
+ return async (props: any, ..._children: string[]) => {
+ if (!renderer) {
+ // Second attempt at resolving a renderer (this time we have props!)
+ renderer = resolveRenderer(Component, props);
+
+ // Okay now we definitely can't resolve a renderer, so let's throw
+ if (!renderer) {
+ const name = typeof Component === 'function' ? (Component.displayName ?? Component.name) : `{ ${Object.keys(Component).join(', ')} }`;
+ throw new Error(`No renderer found for ${name}! Did you forget to add a renderer to your Astro config?`);
+ }
+ }
+ const children = _children.join('\n');
+ const { html } = await renderer.renderToStaticMarkup(Component, props, children);
+ // If we're NOT hydrating this component, just return the HTML
+ if (!componentProps.hydrate) {
+ // It's safe to remove <astro-fragment>, static content doesn't need the wrapper
+ return html.replace(/\<\/?astro-fragment\>/g, '');
+ };
+
+ // If we ARE hydrating this component, let's generate the hydration script
+ const astroId = hash.unique(html);
+ const script = await generateHydrateScript({ renderer, astroId, props }, componentProps as Required<AstroComponentProps>)
+ const astroRoot = `<astro-root uid="${astroId}">${html}</astro-root>`;
+ return [astroRoot, script].join('\n');
+ }
+}
diff --git a/packages/astro/src/frontend/hydrate/idle.ts b/packages/astro/src/frontend/hydrate/idle.ts
new file mode 100644
index 000000000..2fd96b9cb
--- /dev/null
+++ b/packages/astro/src/frontend/hydrate/idle.ts
@@ -0,0 +1,23 @@
+import type { GetHydrateCallback } from '../../@types/hydrate';
+
+/**
+ * Hydrate this component as soon as the main thread is free
+ * (or after a short delay, if `requestIdleCallback`) isn't supported
+ */
+export default async function onIdle(astroId: string, getHydrateCallback: GetHydrateCallback) {
+ const cb = async () => {
+ const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
+ const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
+ const hydrate = await getHydrateCallback();
+
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
+ };
+
+ if ('requestIdleCallback' in window) {
+ (window as any).requestIdleCallback(cb);
+ } else {
+ setTimeout(cb, 200);
+ }
+}
diff --git a/packages/astro/src/frontend/hydrate/load.ts b/packages/astro/src/frontend/hydrate/load.ts
new file mode 100644
index 000000000..38ac1a0ea
--- /dev/null
+++ b/packages/astro/src/frontend/hydrate/load.ts
@@ -0,0 +1,14 @@
+import type { GetHydrateCallback } from '../../@types/hydrate';
+
+/**
+ * Hydrate this component immediately
+ */
+export default async function onLoad(astroId: string, getHydrateCallback: GetHydrateCallback) {
+ const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
+ const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
+ const hydrate = await getHydrateCallback();
+
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
+}
diff --git a/packages/astro/src/frontend/hydrate/visible.ts b/packages/astro/src/frontend/hydrate/visible.ts
new file mode 100644
index 000000000..d4dacdf51
--- /dev/null
+++ b/packages/astro/src/frontend/hydrate/visible.ts
@@ -0,0 +1,32 @@
+import type { GetHydrateCallback } from '../../@types/hydrate';
+
+/**
+ * Hydrate this component when one of it's children becomes visible.
+ * We target the children because `astro-root` is set to `display: contents`
+ * which doesn't work with IntersectionObserver
+ */
+export default async function onVisible(astroId: string, getHydrateCallback: GetHydrateCallback) {
+ const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
+ const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
+
+ const cb = async () => {
+ const hydrate = await getHydrateCallback();
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
+ };
+
+ const io = new IntersectionObserver(([entry]) => {
+ if (!entry.isIntersecting) return;
+ // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
+ io.disconnect();
+ cb();
+ });
+
+ for (const root of roots) {
+ for (let i = 0; i < root.children.length; i++) {
+ const child = root.children[i];
+ io.observe(child);
+ }
+ }
+}
diff --git a/packages/astro/src/frontend/render/preact.ts b/packages/astro/src/frontend/render/preact.ts
deleted file mode 100644
index 5c50b6fe3..000000000
--- a/packages/astro/src/frontend/render/preact.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { h, render, ComponentType } from 'preact';
-import { renderToString } from 'preact-render-to-string';
-import { childrenToVnodes } from './utils';
-import type { ComponentRenderer } from '../../@types/renderer';
-import { createRenderer } from './renderer';
-
-// This prevents tree-shaking of render.
-Function.prototype(render);
-
-const Preact: ComponentRenderer<ComponentType> = {
- jsxPragma: h,
- jsxPragmaName: 'h',
- renderStatic(Component) {
- return async (props, ...children) => {
- return renderToString(h(Component, props, childrenToVnodes(h, children)));
- };
- },
- imports: {
- preact: ['render', 'Fragment', 'h'],
- },
- render({ Component, root, props, children }) {
- return `render(h(${Component}, ${props}, h(Fragment, null, ...${children})), ${root})`;
- },
-};
-
-const renderer = createRenderer(Preact);
-
-export const __preact_static = renderer.static;
-export const __preact_load = renderer.load;
-export const __preact_idle = renderer.idle;
-export const __preact_visible = renderer.visible;
diff --git a/packages/astro/src/frontend/render/react.ts b/packages/astro/src/frontend/render/react.ts
deleted file mode 100644
index 063c6a2b5..000000000
--- a/packages/astro/src/frontend/render/react.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { ComponentRenderer } from '../../@types/renderer';
-import React, { ComponentType } from 'react';
-import ReactDOMServer from 'react-dom/server';
-import { createRenderer } from './renderer';
-import { childrenToVnodes } from './utils';
-
-// This prevents tree-shaking of render.
-Function.prototype(ReactDOMServer);
-
-const ReactRenderer: ComponentRenderer<ComponentType> = {
- jsxPragma: React.createElement,
- jsxPragmaName: 'React.createElement',
- renderStatic(Component) {
- return async (props, ...children) => {
- return ReactDOMServer.renderToString(React.createElement(Component, props, childrenToVnodes(React.createElement, children)));
- };
- },
- imports: {
- react: ['default: React'],
- 'react-dom': ['default: ReactDOM'],
- },
- render({ Component, root, children, props }) {
- return `ReactDOM.hydrate(React.createElement(${Component}, ${props}, React.createElement(React.Fragment, null, ...${children})), ${root})`;
- },
-};
-
-const renderer = createRenderer(ReactRenderer);
-
-export const __react_static = renderer.static;
-export const __react_load = renderer.load;
-export const __react_idle = renderer.idle;
-export const __react_visible = renderer.visible;
diff --git a/packages/astro/src/frontend/render/renderer.ts b/packages/astro/src/frontend/render/renderer.ts
deleted file mode 100644
index a7f6d165d..000000000
--- a/packages/astro/src/frontend/render/renderer.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { DynamicRenderContext, DynamicRendererGenerator, SupportedComponentRenderer, StaticRendererGenerator } from '../../@types/renderer';
-import { childrenToH } from './utils';
-
-// This prevents tree-shaking of render.
-Function.prototype(childrenToH);
-
-/** Initialize Astro Component renderer for Static and Dynamic components */
-export function createRenderer(renderer: SupportedComponentRenderer) {
- const _static: StaticRendererGenerator = (Component) => renderer.renderStatic(Component);
- const _imports = (context: DynamicRenderContext) => {
- const values = Object.values(renderer.imports ?? {})
- .reduce((acc, v) => {
- return [...acc, `{ ${v.join(', ')} }`];
- }, [])
- .join(', ');
- const libs = Object.keys(renderer.imports ?? {})
- .reduce((acc: string[], lib: string) => {
- return [...acc, `import("${context.frameworkUrls[lib as any]}")`];
- }, [])
- .join(',');
- return `const [{${context.componentExport}: Component}, ${values}] = await Promise.all([import("${context.componentUrl}")${renderer.imports ? ', ' + libs : ''}]);`;
- };
- const serializeProps = ({ children: _, ...props }: Record<string, any>) => JSON.stringify(props);
- const createContext = () => {
- const astroId = `${Math.floor(Math.random() * 1e16)}`;
- return { ['data-astro-id']: astroId, root: `document.querySelector('[data-astro-id="${astroId}"]')`, Component: 'Component' };
- };
- const createDynamicRender: DynamicRendererGenerator = (wrapperStart, wrapperEnd) => (Component, renderContext) => {
- const innerContext = createContext();
- return async (props, ...children) => {
- let value: string;
- try {
- value = await _static(Component)(props, ...children);
- } catch (e) {
- value = '';
- }
- value = `<div data-astro-id="${innerContext['data-astro-id']}" style="display:contents">${value}</div>`;
-
- const script = `${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart}
-${_imports(renderContext)}
-${renderer.render({
- ...innerContext,
- props: serializeProps(props),
- children: `[${childrenToH(renderer, children) ?? ''}]`,
- childrenAsString: `\`${children}\``,
-})}
-${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd}`;
-
- return [value, `<script type="module">${script.trim()}</script>`].join('\n');
- };
- };
-
- return {
- static: _static,
- load: createDynamicRender('(async () => {', '})()'),
- idle: createDynamicRender('requestIdleCallback(async () => {', '})'),
- visible: createDynamicRender(
- 'const o = new IntersectionObserver(async ([entry]) => { if (!entry.isIntersecting) { return; } o.disconnect();',
- ({ root }) => `}); Array.from(${root}.children).forEach(child => o.observe(child))`
- ),
- };
-}
diff --git a/packages/astro/src/frontend/render/svelte.ts b/packages/astro/src/frontend/render/svelte.ts
deleted file mode 100644
index 69c7ac8b4..000000000
--- a/packages/astro/src/frontend/render/svelte.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { ComponentRenderer } from '../../@types/renderer';
-import type { SvelteComponent } from 'svelte';
-import { createRenderer } from './renderer';
-import SvelteWrapper from '../SvelteWrapper.server.svelte';
-
-const SvelteRenderer: ComponentRenderer<SvelteComponent> = {
- renderStatic(Component) {
- return async (props, ...children) => {
- /// @ts-expect-error
- const { html } = SvelteWrapper.render({ __astro_component: Component, __astro_children: children.join('\n'), ...props });
- return html;
- };
- },
- imports: {
- 'svelte-runtime': ['default: render'],
- },
- render({ Component, root, props, childrenAsString }) {
- return `render(${root}, ${Component}, ${props}, ${childrenAsString});`;
- },
-};
-
-const renderer = createRenderer(SvelteRenderer);
-
-export const __svelte_static = renderer.static;
-export const __svelte_load = renderer.load;
-export const __svelte_idle = renderer.idle;
-export const __svelte_visible = renderer.visible;
diff --git a/packages/astro/src/frontend/render/utils.ts b/packages/astro/src/frontend/render/utils.ts
deleted file mode 100644
index 64a712561..000000000
--- a/packages/astro/src/frontend/render/utils.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import unified from 'unified';
-import parse from 'rehype-parse';
-import toH from 'hast-to-hyperscript';
-import { ComponentRenderer } from '../../@types/renderer';
-import moize from 'moize';
-
-/** @internal */
-function childrenToTree(children: string[]): any[] {
- return [].concat(...children.map((child) => (unified().use(parse, { fragment: true }).parse(child) as any).children));
-}
-
-/**
- * Converts an HTML fragment string into vnodes for rendering via provided framework
- * @param h framework's `createElement` function
- * @param children the HTML string children
- */
-export const childrenToVnodes = moize.deep(function childrenToVnodes(h: any, children: string[]) {
- const tree = childrenToTree(children);
- const vnodes = tree.map((subtree) => {
- if (subtree.type === 'text') return subtree.value;
- return toH(h, subtree);
- });
- return vnodes;
-});
-
-/**
- * Converts an HTML fragment string into h function calls as a string
- * @param h framework's `createElement` function
- * @param children the HTML string children
- */
-export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRenderer<any>, children: string[]): any {
- if (!renderer.jsxPragma) return;
-
- const tree = childrenToTree(children);
- const innerH = (name: any, attrs: Record<string, any> | null = null, _children: string[] | null = null) => {
- const vnode = renderer.jsxPragma?.(name, attrs, _children);
- const childStr = _children ? `, [${_children.map((child) => serializeChild(child)).join(',')}]` : '';
- if (attrs && attrs.key) attrs.key = Math.random();
- const __SERIALIZED = `${renderer.jsxPragmaName}("${name}", ${attrs ? JSON.stringify(attrs) : 'null'}${childStr})` as string;
- return { ...vnode, __SERIALIZED };
- };
-
- const simpleTypes = new Set(['number', 'boolean']);
- const serializeChild = (child: unknown) => {
- if (typeof child === 'string') return JSON.stringify(child).replace(/<\/script>/gim, '</script" + ">');
- if (simpleTypes.has(typeof child)) return JSON.stringify(child);
- if (child === null) return `null`;
- if ((child as any).__SERIALIZED) return (child as any).__SERIALIZED;
- return innerH(child).__SERIALIZED;
- };
- return tree.map((subtree) => {
- if (subtree.type === 'text') return JSON.stringify(subtree.value);
- return toH(innerH, subtree).__SERIALIZED;
- });
-});
diff --git a/packages/astro/src/frontend/render/vue.ts b/packages/astro/src/frontend/render/vue.ts
deleted file mode 100644
index 57c3c8276..000000000
--- a/packages/astro/src/frontend/render/vue.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { ComponentRenderer } from '../../@types/renderer';
-import type { Component as VueComponent } from 'vue';
-import { renderToString } from '@vue/server-renderer';
-import { defineComponent, createSSRApp, h as createElement } from 'vue';
-import { createRenderer } from './renderer';
-
-// This prevents tree-shaking of render.
-Function.prototype(renderToString);
-
-/**
- * Users might attempt to use :vueAttribute syntax to pass primitive values.
- * If so, try to JSON.parse them to get the primitives
- */
-function cleanPropsForVue(obj: Record<string, any>) {
- let cleaned = {} as any;
- for (let [key, value] of Object.entries(obj)) {
- if (key.startsWith(':')) {
- key = key.slice(1);
- if (typeof value === 'string') {
- try {
- value = JSON.parse(value);
- } catch (e) {}
- }
- }
- cleaned[key] = value;
- }
- return cleaned;
-}
-
-const Vue: ComponentRenderer<VueComponent> = {
- jsxPragma: createElement,
- jsxPragmaName: 'createElement',
- renderStatic(Component) {
- return async (props, ...children) => {
- const App = defineComponent({
- components: {
- Component,
- },
- data() {
- return { props };
- },
- template: `<Component v-bind="props">${children.join('\n')}</Component>`,
- });
-
- const app = createSSRApp(App);
- const html = await renderToString(app);
- return html;
- };
- },
- imports: {
- vue: ['createApp', 'h: createElement'],
- },
- render({ Component, root, props, children }) {
- const vueProps = cleanPropsForVue(JSON.parse(props));
- return `const App = { render: () => createElement(${Component}, ${JSON.stringify(vueProps)}, { default: () => ${children} }) };
-createApp(App).mount(${root});`;
- },
-};
-
-const renderer = createRenderer(Vue);
-
-export const __vue_static = renderer.static;
-export const __vue_load = renderer.load;
-export const __vue_idle = renderer.idle;
-export const __vue_visible = renderer.visible;
diff --git a/packages/astro/src/frontend/renderer-astro.ts b/packages/astro/src/frontend/renderer-astro.ts
new file mode 100644
index 000000000..36b74ff8f
--- /dev/null
+++ b/packages/astro/src/frontend/renderer-astro.ts
@@ -0,0 +1,8 @@
+export function check(Component: any) {
+ return Component.isAstroComponent;
+}
+
+export async function renderToStaticMarkup(Component: any, props: any, children: string) {
+ const html = await Component.__render(props, children);
+ return { html }
+};
diff --git a/packages/astro/src/frontend/runtime/svelte.ts b/packages/astro/src/frontend/runtime/svelte.ts
deleted file mode 100644
index 7a41586b4..000000000
--- a/packages/astro/src/frontend/runtime/svelte.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import SvelteWrapper from '../SvelteWrapper.client.svelte';
-import type { SvelteComponent } from 'svelte';
-
-export default (target: Element, component: SvelteComponent, props: any, children: string) => {
- new SvelteWrapper({
- target,
- props: { __astro_component: component, __astro_children: children, ...props },
- hydrate: true,
- });
-};
diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts
index 849642002..bc5762585 100644
--- a/packages/astro/src/runtime.ts
+++ b/packages/astro/src/runtime.ts
@@ -4,18 +4,16 @@ import type { CompileError } from 'astro-parser';
import type { LogOptions } from './logger';
import type { AstroConfig, CollectionResult, CollectionRSS, CreateCollection, Params, RuntimeMode } from './@types/astro';
+import resolve from 'resolve';
import { existsSync } from 'fs';
-import { fileURLToPath } from 'url';
+import { fileURLToPath, pathToFileURL } from 'url';
+import { posix as path } from 'path';
import { performance } from 'perf_hooks';
import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack';
import { canonicalURL, stopTimer } from './build/util.js';
import { debug, info } from './logger.js';
import { searchForPage } from './search.js';
-// We need to use require.resolve for snowpack plugins, so create a require function here.
-import { createRequire } from 'module';
-const require = createRequire(import.meta.url);
-
interface RuntimeConfig {
astroConfig: AstroConfig;
logging: LogOptions;
@@ -268,21 +266,28 @@ interface CreateSnowpackOptions {
resolvePackageUrl?: (pkgName: string) => Promise<string>;
}
+const defaultRenderers = [
+ '@astro-renderer/vue',
+ '@astro-renderer/svelte',
+ '@astro-renderer/react',
+ '@astro-renderer/preact'
+];
+
/** Create a new Snowpack instance to power Astro */
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
- const { projectRoot, astroRoot, extensions } = astroConfig;
+ const { projectRoot, astroRoot, renderers = defaultRenderers } = astroConfig;
const { env, mode, resolvePackageUrl } = options;
const internalPath = new URL('./frontend/', import.meta.url);
+ const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
let snowpack: SnowpackDevServer;
- const astroPlugOptions: {
+ let astroPluginOptions: {
resolvePackageUrl?: (s: string) => Promise<string>;
- extensions?: Record<string, string>;
+ renderers?: { name: string, client: string, server: string }[];
astroConfig: AstroConfig;
} = {
astroConfig,
- extensions,
resolvePackageUrl,
};
@@ -300,22 +305,58 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
(process.env as any).TAILWIND_DISABLE_TOUCH = true;
}
+ const rendererInstances = (await Promise.all(renderers.map(renderer => import(pathToFileURL(resolveDependency(renderer)).toString()))))
+ .map(({ default: raw }, i) => {
+ const { name = renderers[i], client, server, snowpackPlugin: snowpackPluginName, snowpackPluginOptions } = raw;
+
+ if (typeof client !== 'string') {
+ throw new Error(`Expected "client" from ${name} to be a relative path to the client-side renderer!`);
+ }
+
+ if (typeof server !== 'string') {
+ throw new Error(`Expected "server" from ${name} to be a relative path to the server-side renderer!`);
+ }
+
+ let snowpackPlugin: string|[string, any]|undefined;
+ if (typeof snowpackPluginName === 'string') {
+ if (snowpackPluginOptions) {
+ snowpackPlugin = [resolveDependency(snowpackPluginName), snowpackPluginOptions]
+ } else {
+ snowpackPlugin = resolveDependency(snowpackPluginName);
+ }
+ } else if (snowpackPluginName) {
+ throw new Error(`Expected the snowpackPlugin from ${name} to be a "string" but encountered "${typeof snowpackPluginName}"!`);
+ }
+
+ return {
+ name,
+ snowpackPlugin,
+ client: path.join(name, raw.client),
+ server: path.join(name, raw.server),
+ }
+ })
+
+ astroPluginOptions.renderers = rendererInstances;
+
+ // Make sure that Snowpack builds our renderer plugins
+ const knownEntrypoints = [].concat(...rendererInstances.map(renderer => [renderer.server, renderer.client]) as any) as string[];
+ const rendererSnowpackPlugins = rendererInstances.filter(renderer => renderer.snowpackPlugin).map(renderer => renderer.snowpackPlugin) as string|[string, any];
+
const snowpackConfig = await loadConfiguration({
root: fileURLToPath(projectRoot),
mount: mountOptions,
mode,
plugins: [
- [fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
- [require.resolve('@snowpack/plugin-svelte'), { compilerOptions: { hydratable: true } }],
- require.resolve('@snowpack/plugin-vue'),
- require.resolve('@snowpack/plugin-sass'),
+ [fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPluginOptions],
+ ...rendererSnowpackPlugins,
+ resolveDependency('@snowpack/plugin-sass'),
[
- require.resolve('@snowpack/plugin-postcss'),
+ resolveDependency('@snowpack/plugin-postcss'),
{
config: {
plugins: {
- [require.resolve('autoprefixer')]: {},
- ...(astroConfig.devOptions.tailwindConfig ? { [require.resolve('tailwindcss')]: {} } : {}),
+ [resolveDependency('autoprefixer')]: {},
+ ...(astroConfig.devOptions.tailwindConfig ? { [resolveDependency('autoprefixer')]: {} } : {}),
},
},
},
@@ -331,7 +372,7 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
out: astroConfig.dist,
},
packageOptions: {
- knownEntrypoints: ['preact-render-to-string'],
+ knownEntrypoints,
external: ['@vue/server-renderer', 'node-fetch', 'prismjs/components/index.js', 'gray-matter'],
},
});