Renderer plugins (#231)
* refactor: pluggable renderers * refactor: cache renderer per component * docs: update comments on snowpack plugin `transform` method * docs: add comments to renderer plugins * refactor: convert components to Map * fix: pass children through to astro __render * refactor: move Components/ComponentInfo to shared types * refactor: remove `gatherRuntimes` step, just scan output for imports * refactor: update isComponentTag logic * chore: move dependencies to renderers * fix: cross-platform transform injection * feat: defer renderer to react, fallback to preact * fix: use double quotes in generated script * test: fix failing children tests * test: add workspaceRoot to all tests * fix: pass props to renderer check * chore: add test:core script back for convenience * chore: remove unused external * chore: rename renderers * chore: add astring, estree-util-value-to-estree * chore: render-component => __astro_component * refactor: split hydrate logic to own file * refactor: use `astro-fragment` rather than `div` * chore: remove unused hooks * chore: delete unused file * chore: add changesets * fix: Astro renderer should be async * fix: remove <astro-fragment> for static content * test: fix failing test * chore: normalize config interface * feat: allow renderers to inject a snowpackPlugin * fix: resolve import URL before using dynamic import * refactor: update renderers to use separate /server entrypoint * refactor: update server renderer interface * fix: get renderers working again * test: debug failing test * test: better debug * test: better debug * test: remove debug * fix: support esm and cjs packages via "resolve" * refactor: split hydrate functions into individual files * fix: dependency resolution relative to projectRoot * fix: @snowpack/plugin-postcss needs to be hoisted * fix: do not test prettier-plugin-astro as it's not ready for primetime
This commit is contained in:
parent
31e52c2e4c
commit
643c880f28
73 changed files with 2466 additions and 2444 deletions
18
.changeset/shaggy-countries-battle.md
Normal file
18
.changeset/shaggy-countries-battle.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
**This is a breaking change**
|
||||
|
||||
Updated the rendering pipeline for `astro` to truly support any framework.
|
||||
|
||||
For the vast majority of use cases, `astro` should _just work_ out of the box. Astro now depends on `@astro-renderer/preact`, `@astro-renderer/react`, `@astro-renderer/svelte`, and `@astro-renderer/vue`, rather than these being built into the core library. This opens the door for anyone to contribute additional renderers for Astro to support their favorite framework, as well as the ability for users to control which renderers should be used.
|
||||
|
||||
**Features**
|
||||
- Expose a pluggable interface for controlling server-side rendering and client-side hydration
|
||||
- Allows components from different frameworks to be nested within each other.
|
||||
> Note: `svelte` currently does support non-destructive hydration, so components from other frameworks cannot currently be nested inside of a Svelte component. See https://github.com/sveltejs/svelte/issues/4308.
|
||||
|
||||
**Breaking Changes**
|
||||
- To improve compiler performance, improve framework support, and minimize JS payloads, any children passed to hydrated components are automatically wrapped with an `<astro-fragment>` element.
|
||||
|
8
.changeset/smooth-toes-tan.md
Normal file
8
.changeset/smooth-toes-tan.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
'@astro-renderer/preact': minor
|
||||
'@astro-renderer/react': minor
|
||||
'@astro-renderer/svelte': minor
|
||||
'@astro-renderer/vue': minor
|
||||
---
|
||||
|
||||
Initial release
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
extensions: {
|
||||
'.jsx': 'react',
|
||||
'.tsx': 'preact',
|
||||
}
|
||||
};
|
3
examples/kitchen-sink/src/components/A.astro
Normal file
3
examples/kitchen-sink/src/components/A.astro
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="children">
|
||||
<h1>Hello Astro (A)</h1>
|
||||
</div>
|
3
examples/kitchen-sink/src/components/B.astro
Normal file
3
examples/kitchen-sink/src/components/B.astro
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="children">
|
||||
<h1>Hello Astro (B)</h1>
|
||||
</div>
|
|
@ -2,7 +2,7 @@ import { h, Fragment } from 'preact';
|
|||
import { useState } from 'preact/hooks';
|
||||
|
||||
/** a counter written in Preact */
|
||||
export default function PreactCounter({ children }) {
|
||||
export function PreactCounter({ children }) {
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
/** a counter written in React */
|
||||
export default function ReactCounter({ children }) {
|
||||
export function Counter({ children }) {
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
|
2
examples/kitchen-sink/src/components/index.ts
Normal file
2
examples/kitchen-sink/src/components/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as A } from './A.astro';
|
||||
export { default as B } from './B.astro';
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import ReactCounter from '../components/ReactCounter.jsx';
|
||||
import PreactCounter from '../components/PreactCounter.tsx';
|
||||
import { A, B as Renamed } from '../components';
|
||||
import * as react from '../components/ReactCounter.jsx';
|
||||
import { PreactCounter } from '../components/PreactCounter.tsx';
|
||||
import VueCounter from '../components/VueCounter.vue';
|
||||
import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||
---
|
||||
|
@ -30,22 +31,28 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
|
|||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<ReactCounter:load>
|
||||
|
||||
<react.Counter:visible>
|
||||
<h1>Hello React!</h1>
|
||||
<p>What's up?</p>
|
||||
</ReactCounter:load>
|
||||
</react.Counter:visible>
|
||||
|
||||
<PreactCounter:load>
|
||||
<h1>Hello Preact!</h1>
|
||||
</PreactCounter:load>
|
||||
<PreactCounter:visible>
|
||||
<h1>Hello Preact!</h1>
|
||||
</PreactCounter:visible>
|
||||
|
||||
<VueCounter:load>
|
||||
<VueCounter:visible>
|
||||
<h1>Hello Vue!</h1>
|
||||
</VueCounter:load>
|
||||
</VueCounter:visible>
|
||||
|
||||
<SvelteCounter:load>
|
||||
<SvelteCounter:visible>
|
||||
<h1>Hello Svelte!</h1>
|
||||
</SvelteCounter:load>
|
||||
</SvelteCounter:visible>
|
||||
|
||||
<A />
|
||||
|
||||
<Renamed />
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -10,10 +10,12 @@
|
|||
"dev:vscode": "lerna run dev --scope astro-languageserver --scope astro-vscode --scope astro-parser --parallel --stream",
|
||||
"format": "prettier -w \"**/*.{js,jsx,ts,tsx,md,json}\"",
|
||||
"lint": "eslint \"packages/**/*.ts\"",
|
||||
"test": "lerna run test --scope astro --scope prettier-plugin-astro --scope create-astro --stream"
|
||||
"test": "lerna run test --scope astro --scope create-astro --stream",
|
||||
"test:core": "yarn workspace astro run test"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/renderers/*",
|
||||
"examples/*",
|
||||
"tools/*",
|
||||
"scripts",
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
"test": "uvu test -i fixtures -i benchmark -i test-utils.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astro-renderer/preact": "0.0.1",
|
||||
"@astro-renderer/react": "0.0.1",
|
||||
"@astro-renderer/svelte": "0.0.1",
|
||||
"@astro-renderer/vue": "0.0.1",
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.9",
|
||||
"@babel/parser": "^7.13.15",
|
||||
|
@ -39,10 +43,8 @@
|
|||
"@silvenon/remark-smartypants": "^1.0.0",
|
||||
"@snowpack/plugin-postcss": "^1.4.0",
|
||||
"@snowpack/plugin-sass": "^1.4.0",
|
||||
"@snowpack/plugin-svelte": "^3.7.0",
|
||||
"@snowpack/plugin-vue": "^2.5.0",
|
||||
"@vue/server-renderer": "^3.0.10",
|
||||
"acorn": "^7.4.0",
|
||||
"astring": "^1.7.4",
|
||||
"astro-parser": "0.11.0",
|
||||
"astro-prism": "0.0.2",
|
||||
"autoprefixer": "^10.2.5",
|
||||
|
@ -50,6 +52,7 @@
|
|||
"del": "^6.0.0",
|
||||
"es-module-lexer": "^0.4.1",
|
||||
"esbuild": "^0.10.1",
|
||||
"estree-util-value-to-estree": "^1.2.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"fdir": "^5.0.0",
|
||||
|
@ -69,11 +72,7 @@
|
|||
"picomatch": "^2.2.3",
|
||||
"postcss": "^8.2.15",
|
||||
"postcss-icss-keyframes": "^0.2.1",
|
||||
"preact": "^10.5.13",
|
||||
"preact-render-to-string": "^5.1.18",
|
||||
"prismjs": "^1.23.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"rehype-parse": "^7.0.1",
|
||||
"rehype-raw": "^5.1.0",
|
||||
"rehype-stringify": "^8.0.0",
|
||||
|
@ -81,6 +80,7 @@
|
|||
"remark-gfm": "^1.0.0",
|
||||
"remark-parse": "^9.0.0",
|
||||
"remark-rehype": "^8.1.0",
|
||||
"resolve": "^1.20.0",
|
||||
"rollup": "^2.43.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sass": "^1.32.13",
|
||||
|
@ -89,10 +89,8 @@
|
|||
"snowpack": "^3.5.1",
|
||||
"source-map-support": "^0.5.19",
|
||||
"string-width": "^5.0.0",
|
||||
"svelte": "^3.35.0",
|
||||
"tiny-glob": "^0.2.8",
|
||||
"unified": "^9.2.1",
|
||||
"vue": "^3.0.10",
|
||||
"yargs-parser": "^20.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -3,14 +3,45 @@ const { readFile } = require('fs').promises;
|
|||
// Snowpack plugins must be CommonJS :(
|
||||
const transformPromise = import('./dist/compiler/index.js');
|
||||
|
||||
module.exports = function (snowpackConfig, { resolvePackageUrl, extensions, astroConfig } = {}) {
|
||||
module.exports = function (snowpackConfig, { resolvePackageUrl, renderers, astroConfig } = {}) {
|
||||
return {
|
||||
name: 'snowpack-astro',
|
||||
knownEntrypoints: [],
|
||||
resolve: {
|
||||
input: ['.astro', '.md'],
|
||||
output: ['.js', '.css'],
|
||||
},
|
||||
/**
|
||||
* This injects our renderer plugins to the Astro runtime (as a bit of a hack).
|
||||
*
|
||||
* In a world where Snowpack supports virtual files, this won't be necessary and
|
||||
* should be refactored to a virtual file that is imported by the runtime.
|
||||
*
|
||||
* Take a look at `/src/frontend/__astro_component.ts`. It relies on both
|
||||
* `__rendererSources` and `__renderers` being defined, so we're creating those here.
|
||||
*
|
||||
* The output of this is the following (or something very close to it):
|
||||
*
|
||||
* ```js
|
||||
* import * as __renderer_0 from '/_snowpack/link/packages/renderers/vue/index.js';
|
||||
* import * as __renderer_1 from '/_snowpack/link/packages/renderers/svelte/index.js';
|
||||
* import * as __renderer_2 from '/_snowpack/link/packages/renderers/preact/index.js';
|
||||
* import * as __renderer_3 from '/_snowpack/link/packages/renderers/react/index.js';
|
||||
* let __rendererSources = ["/_snowpack/link/packages/renderers/vue/client.js", "/_snowpack/link/packages/renderers/svelte/client.js", "/_snowpack/link/packages/renderers/preact/client.js", "/_snowpack/link/packages/renderers/react/client.js"];
|
||||
* let __renderers = [__renderer_0, __renderer_1, __renderer_2, __renderer_3];
|
||||
* // the original file contents
|
||||
* ```
|
||||
*/
|
||||
async transform({contents, id, fileExt}) {
|
||||
if (fileExt === '.js' && /__astro_component\.js/g.test(id)) {
|
||||
const rendererServerPackages = await Promise.all(renderers.map(({ server }) => resolvePackageUrl(server)));
|
||||
const rendererClientPackages = await Promise.all(renderers.map(({ client }) => resolvePackageUrl(client)));
|
||||
const result = `${rendererServerPackages.map((pkg, i) => `import __renderer_${i} from "${pkg}";`).join('\n')}
|
||||
let __rendererSources = [${rendererClientPackages.map(pkg => `"${pkg}"`).join(', ')}];
|
||||
let __renderers = [${rendererServerPackages.map((_, i) => `__renderer_${i}`).join(', ')}];
|
||||
${contents}`;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
async load({ filePath }) {
|
||||
const { compileComponent } = await transformPromise;
|
||||
const projectRoot = snowpackConfig.root;
|
||||
|
@ -18,7 +49,7 @@ module.exports = function (snowpackConfig, { resolvePackageUrl, extensions, astr
|
|||
const compileOptions = {
|
||||
astroConfig,
|
||||
resolvePackageUrl,
|
||||
extensions,
|
||||
renderers,
|
||||
};
|
||||
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
|
||||
const output = {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
1
packages/astro/src/@types/hydrate.ts
Normal file
1
packages/astro/src/@types/hydrate.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string|null) => void>;
|
1
packages/astro/src/@types/resolve.d.ts
vendored
Normal file
1
packages/astro/src/@types/resolve.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'resolve';
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
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: 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')}';`,
|
||||
};
|
||||
case 'ImportNamespaceSpecifier': {
|
||||
const [_, value] = name.split('.');
|
||||
return { value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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')}';`,
|
||||
};
|
||||
}
|
||||
|
||||
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')}';`,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
63
packages/astro/src/compiler/transform/hydration.ts
Normal file
63
packages/astro/src/compiler/transform/hydration.ts
Normal file
|
@ -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];
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
const { __astro_component: Component, __astro_children, ...props } = $$props;
|
||||
</script>
|
||||
|
||||
<svelte:component this={Component} {...props}>
|
||||
{@html __astro_children}
|
||||
</svelte:component>
|
100
packages/astro/src/frontend/__astro_component.ts
Normal file
100
packages/astro/src/frontend/__astro_component.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
23
packages/astro/src/frontend/hydrate/idle.ts
Normal file
23
packages/astro/src/frontend/hydrate/idle.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
14
packages/astro/src/frontend/hydrate/load.ts
Normal file
14
packages/astro/src/frontend/hydrate/load.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
32
packages/astro/src/frontend/hydrate/visible.ts
Normal file
32
packages/astro/src/frontend/hydrate/visible.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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))`
|
||||
),
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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;
|
8
packages/astro/src/frontend/renderer-astro.ts
Normal file
8
packages/astro/src/frontend/renderer-astro.ts
Normal file
|
@ -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 }
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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'],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ setupBuild(ComponentChildren, './fixtures/astro-children');
|
|||
|
||||
ComponentChildren('Passes string children to framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/strings');
|
||||
if (result.error) throw new Error(result.error);
|
||||
if (result.error) throw new Error(result);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
|
@ -30,19 +30,22 @@ ComponentChildren('Passes markup children to framework components', async ({ run
|
|||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const $preact = $('#preact > h1');
|
||||
const $preact = $('#preact h1');
|
||||
assert.equal($preact.text().trim(), 'Hello world', 'Can pass markup to Preact components');
|
||||
|
||||
const $vue = $('#vue > h1');
|
||||
const $vue = $('#vue h1');
|
||||
assert.equal($vue.text().trim(), 'Hello world', 'Can pass markup to Vue components');
|
||||
|
||||
const $svelte = $('#svelte > h1');
|
||||
const $svelte = $('#svelte h1');
|
||||
assert.equal($svelte.text().trim(), 'Hello world', 'Can pass markup to Svelte components');
|
||||
});
|
||||
|
||||
ComponentChildren('Passes multiple children to framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/multiple');
|
||||
if (result.error) throw new Error(result.error);
|
||||
if (result.error) {
|
||||
console.log(result);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
|
|
|
@ -13,17 +13,17 @@ DynamicComponents('Loads client-only packages', async ({ runtime }) => {
|
|||
|
||||
// Grab the react-dom import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, reactDomURL;
|
||||
let match, reactRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('react-dom')) {
|
||||
reactDomURL = match[1];
|
||||
if (match[1].includes('renderers/react/client.js')) {
|
||||
reactRenderer = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(reactDomURL, 'React dom is on the page');
|
||||
assert.ok(reactRenderer, 'React renderer is on the page');
|
||||
|
||||
result = await runtime.load(reactDomURL);
|
||||
assert.equal(result.statusCode, 200, 'Can load react-dom');
|
||||
result = await runtime.load(reactRenderer);
|
||||
assert.equal(result.statusCode, 200, 'Can load react renderer');
|
||||
});
|
||||
|
||||
DynamicComponents('Can be built', async ({ build }) => {
|
||||
|
|
3
packages/astro/test/fixtures/astro-basic/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-basic/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-children/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-children/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-collection/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-collection/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-css-bundling/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-css-bundling/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-doctype/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-doctype/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-dynamic/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-dynamic/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-expr/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-expr/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-fallback/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-fallback/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-global/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-global/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-rss/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-rss/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/astro-styles-ssr/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-styles-ssr/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
3
packages/astro/test/fixtures/react-component/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/react-component/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
4
packages/renderers/preact/client.js
Normal file
4
packages/renderers/preact/client.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { h, hydrate } from 'preact';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
export default (element) => (Component, props, children) => hydrate(h(Component, props, h(StaticHtml, { value: children })), element);
|
5
packages/renderers/preact/index.js
Normal file
5
packages/renderers/preact/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
name: '@astro-renderer/preact',
|
||||
client: './client',
|
||||
server: './server',
|
||||
}
|
15
packages/renderers/preact/package.json
Normal file
15
packages/renderers/preact/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@astro-renderer/preact",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./server": "./server.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.5.13",
|
||||
"preact-render-to-string": "^5.1.18"
|
||||
}
|
||||
}
|
20
packages/renderers/preact/server.js
Normal file
20
packages/renderers/preact/server.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { h } from 'preact';
|
||||
import { renderToString } from 'preact-render-to-string';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
function check(Component, props) {
|
||||
try {
|
||||
return Boolean(renderToString(h(Component, props)));
|
||||
} catch (e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
function renderToStaticMarkup(Component, props, children) {
|
||||
const html = renderToString(h(Component, props, h(StaticHtml, { value: children })))
|
||||
return { html };
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup
|
||||
}
|
24
packages/renderers/preact/static-html.js
Normal file
24
packages/renderers/preact/static-html.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
/**
|
||||
* Astro passes `children` as a string of HTML, so we need
|
||||
* a wrapper `div` to render that content as VNodes.
|
||||
*
|
||||
* As a bonus, we can signal to Preact that this subtree is
|
||||
* entirely static and will never change via `shouldComponentUpdate`.
|
||||
*/
|
||||
const StaticHtml = ({ value }) => {
|
||||
if (!value) return null;
|
||||
return h('astro-fragment', { dangerouslySetInnerHTML: { __html: value }});
|
||||
}
|
||||
|
||||
/**
|
||||
* This tells Preact to opt-out of re-rendering this subtree,
|
||||
* In addition to being a performance optimization,
|
||||
* this also allows other frameworks to attach to `children`.
|
||||
*
|
||||
* See https://preactjs.com/guide/v8/external-dom-mutations
|
||||
*/
|
||||
StaticHtml.shouldComponentUpdate = () => false;
|
||||
|
||||
export default StaticHtml;
|
5
packages/renderers/react/client.js
Normal file
5
packages/renderers/react/client.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createElement } from 'react';
|
||||
import { hydrate } from 'react-dom';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
export default (element) => (Component, props, children) => hydrate(createElement(Component, { ...props, suppressHydrationWarning: true }, createElement(StaticHtml, { value: children, suppressHydrationWarning: true })), element);
|
5
packages/renderers/react/index.js
Normal file
5
packages/renderers/react/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
name: '@astro-renderer/react',
|
||||
client: './client',
|
||||
server: './server',
|
||||
}
|
15
packages/renderers/react/package.json
Normal file
15
packages/renderers/react/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@astro-renderer/react",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./server": "./server.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
20
packages/renderers/react/server.js
Normal file
20
packages/renderers/react/server.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { createElement as h } from 'react';
|
||||
import { renderToStaticMarkup as renderToString } from 'react-dom/server.js';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
function check(Component, props) {
|
||||
try {
|
||||
return Boolean(renderToString(h(Component, props)));
|
||||
} catch (e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
function renderToStaticMarkup(Component, props, children) {
|
||||
const html = renderToString(h(Component, props, h(StaticHtml, { value: children })))
|
||||
return { html };
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup
|
||||
};
|
24
packages/renderers/react/static-html.js
Normal file
24
packages/renderers/react/static-html.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createElement as h } from 'react';
|
||||
|
||||
/**
|
||||
* Astro passes `children` as a string of HTML, so we need
|
||||
* a wrapper `div` to render that content as VNodes.
|
||||
*
|
||||
* As a bonus, we can signal to React that this subtree is
|
||||
* entirely static and will never change via `shouldComponentUpdate`.
|
||||
*/
|
||||
const StaticHtml = ({ value }) => {
|
||||
if (!value) return null;
|
||||
return h('astro-fragment', { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: value }});
|
||||
}
|
||||
|
||||
/**
|
||||
* This tells React to opt-out of re-rendering this subtree,
|
||||
* In addition to being a performance optimization,
|
||||
* this also allows other frameworks to attach to `children`.
|
||||
*
|
||||
* See https://preactjs.com/guide/v8/external-dom-mutations
|
||||
*/
|
||||
StaticHtml.shouldComponentUpdate = () => false;
|
||||
|
||||
export default StaticHtml;
|
21
packages/renderers/svelte/Wrapper.svelte
Normal file
21
packages/renderers/svelte/Wrapper.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
/**
|
||||
* Why do we need a wrapper component?
|
||||
*
|
||||
* Astro passes `children` as a string of HTML, so we need
|
||||
* a way to render that content.
|
||||
*
|
||||
* Rather than passing a magical prop which needs special
|
||||
* handling, using this wrapper allows Svelte users to just
|
||||
* use `<slot />` like they would for any other component.
|
||||
*/
|
||||
const { __astro_component: Component, __astro_children, ...props } = $$props;
|
||||
</script>
|
||||
|
||||
<svelte:component this={Component} {...props}>
|
||||
{#if __astro_children}
|
||||
<astro-fragment>
|
||||
{@html __astro_children}
|
||||
</astro-fragment>
|
||||
{/if}
|
||||
</svelte:component>
|
13
packages/renderers/svelte/client.js
Normal file
13
packages/renderers/svelte/client.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import SvelteWrapper from './Wrapper.svelte';
|
||||
|
||||
export default (target) => {
|
||||
return (component, props, children) => {
|
||||
try {
|
||||
new SvelteWrapper({
|
||||
target,
|
||||
props: { __astro_component: component, __astro_children: children, ...props },
|
||||
hydrate: true,
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
7
packages/renderers/svelte/index.js
Normal file
7
packages/renderers/svelte/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
name: '@astro-renderer/svelte',
|
||||
snowpackPlugin: '@snowpack/plugin-svelte',
|
||||
snowpackPluginOptions: { compilerOptions: { hydratable: true } },
|
||||
client: './client',
|
||||
server: './server'
|
||||
};
|
15
packages/renderers/svelte/package.json
Normal file
15
packages/renderers/svelte/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@astro-renderer/svelte",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./server": "./server.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte": "^3.35.0",
|
||||
"@snowpack/plugin-svelte": "^3.7.0"
|
||||
}
|
||||
}
|
15
packages/renderers/svelte/server.js
Normal file
15
packages/renderers/svelte/server.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import SvelteWrapper from './Wrapper.svelte';
|
||||
|
||||
function check(Component) {
|
||||
return Component['render'] && Component['$$render'];
|
||||
}
|
||||
|
||||
async function renderToStaticMarkup (Component, props, children) {
|
||||
const { html } = SvelteWrapper.render({ __astro_component: Component, __astro_children: children, ...props });
|
||||
return { html };
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup
|
||||
};
|
7
packages/renderers/vue/client.js
Normal file
7
packages/renderers/vue/client.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { h, createSSRApp } from 'vue';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
export default (element) => (Component, props, children) => {
|
||||
const app = createSSRApp({ render: () => h(Component, props, { default: () => h(StaticHtml, { value: children }) })});
|
||||
app.mount(element, true);
|
||||
};
|
6
packages/renderers/vue/index.js
Normal file
6
packages/renderers/vue/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
name: '@astro-renderer/vue',
|
||||
snowpackPlugin: '@snowpack/plugin-vue',
|
||||
client: './client',
|
||||
server: './server'
|
||||
};
|
16
packages/renderers/vue/package.json
Normal file
16
packages/renderers/vue/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@astro-renderer/vue",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./server": "./server.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.0.10",
|
||||
"@vue/server-renderer": "^3.0.10",
|
||||
"@snowpack/plugin-vue": "^2.5.0"
|
||||
}
|
||||
}
|
18
packages/renderers/vue/server.js
Normal file
18
packages/renderers/vue/server.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { renderToString } from '@vue/server-renderer';
|
||||
import { h, createSSRApp } from 'vue';
|
||||
import StaticHtml from './static-html.js';
|
||||
|
||||
function check(Component) {
|
||||
return Component['ssrRender'];
|
||||
}
|
||||
|
||||
async function renderToStaticMarkup(Component, props, children) {
|
||||
const app = createSSRApp({ render: () => h(Component, props, { default: () => h(StaticHtml, { value: children }) })});
|
||||
const html = await renderToString(app);
|
||||
return { html };
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup
|
||||
}
|
27
packages/renderers/vue/static-html.js
Normal file
27
packages/renderers/vue/static-html.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { h, defineComponent } from 'vue';
|
||||
|
||||
/**
|
||||
* Astro passes `children` as a string of HTML, so we need
|
||||
* a wrapper `div` to render that content as VNodes.
|
||||
*
|
||||
* This is the Vue + JSX equivalent of using `<div v-html="value" />`
|
||||
*/
|
||||
const StaticHtml = defineComponent({
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
setup({ value }) {
|
||||
if (!value) return () => null;
|
||||
return () => h('astro-fragment', { innerHTML: value })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Other frameworks have `shouldComponentUpdate` in order to signal
|
||||
* that this subtree is entirely static and will not be updated
|
||||
*
|
||||
* Fortunately, Vue is smart enough to figure that out without any
|
||||
* help from us, so this just works out of the box!
|
||||
*/
|
||||
|
||||
export default StaticHtml;
|
|
@ -477,7 +477,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"begin": "(</?)([A-Z][a-zA-Z0-9-]*)(\\:(load|idle|visible))?",
|
||||
"begin": "(</?)([A-Z][a-zA-Z0-9-\\.]*|[a-z]+\\.[a-zA-Z0-9-]+)(\\:(load|idle|visible))?",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "punctuation.definition.tag.begin.html"
|
||||
|
|
Loading…
Reference in a new issue