Allow renderers configuration to update (#489)
* Start of dynamic renderers * Implementation
This commit is contained in:
parent
f04b82d47e
commit
491ff66603
6 changed files with 218 additions and 98 deletions
|
@ -4,8 +4,22 @@ const transformPromise = import('./dist/compiler/index.js');
|
|||
|
||||
const DEFAULT_HMR_PORT = 12321;
|
||||
|
||||
/** @type {import('snowpack').SnowpackPluginFactory<any>} */
|
||||
module.exports = (snowpackConfig, { resolvePackageUrl, renderers, astroConfig, mode } = {}) => {
|
||||
/**
|
||||
* @typedef {Object} PluginOptions - creates a new type named 'SpecialType'
|
||||
* @prop {import('./src/config_manager').ConfigManager} configManager
|
||||
* @prop {'development' | 'production'} mode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {import('snowpack').SnowpackPluginFactory<PluginOptions>}
|
||||
*/
|
||||
module.exports = (snowpackConfig, options = {}) => {
|
||||
const {
|
||||
resolvePackageUrl,
|
||||
astroConfig,
|
||||
configManager,
|
||||
mode
|
||||
} = options;
|
||||
let hmrPort = DEFAULT_HMR_PORT;
|
||||
return {
|
||||
name: 'snowpack-astro',
|
||||
|
@ -14,36 +28,18 @@ module.exports = (snowpackConfig, { resolvePackageUrl, renderers, astroConfig, m
|
|||
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 = renderers.map(({ server }) => 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;
|
||||
if(configManager.isConfigModule(fileExt, id)) {
|
||||
configManager.configModuleId = id;
|
||||
const source = await configManager.buildSource(contents);
|
||||
return source;
|
||||
}
|
||||
},
|
||||
onChange({ filePath }) {
|
||||
// If the astro.config.mjs file changes, mark the generated config module as changed.
|
||||
if(configManager.isAstroConfig(filePath) && configManager.configModuleId) {
|
||||
this.markChanged(configManager.configModuleId);
|
||||
configManager.markDirty();
|
||||
}
|
||||
},
|
||||
config(snowpackConfig) {
|
||||
|
@ -55,12 +51,13 @@ ${contents}`;
|
|||
const { compileComponent } = await transformPromise;
|
||||
const projectRoot = snowpackConfig.root;
|
||||
const contents = await readFile(filePath, 'utf-8');
|
||||
|
||||
/** @type {import('./src/@types/compiler').CompileOptions} */
|
||||
const compileOptions = {
|
||||
astroConfig,
|
||||
hmrPort,
|
||||
mode,
|
||||
resolvePackageUrl,
|
||||
renderers,
|
||||
};
|
||||
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
|
||||
const output = {
|
||||
|
|
|
@ -176,3 +176,16 @@ export interface ComponentInfo {
|
|||
}
|
||||
|
||||
export type Components = Map<string, ComponentInfo>;
|
||||
|
||||
type AsyncRendererComponentFn<U> = (
|
||||
Component: any,
|
||||
props: any,
|
||||
children: string | undefined
|
||||
) => Promise<U>;
|
||||
|
||||
export interface Renderer {
|
||||
check: AsyncRendererComponentFn<boolean>;
|
||||
renderToStaticMarkup: AsyncRendererComponentFn<{
|
||||
html: string;
|
||||
}>;
|
||||
}
|
140
packages/astro/src/config_manager.ts
Normal file
140
packages/astro/src/config_manager.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import type { ServerRuntime as SnowpackServerRuntime } from 'snowpack';
|
||||
import type { AstroConfig } from './@types/astro';
|
||||
import { posix as path } from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import resolve from 'resolve';
|
||||
import { loadConfig } from './config.js';
|
||||
|
||||
type RendererSnowpackPlugin = string | [string, any] | undefined;
|
||||
|
||||
interface RendererInstance {
|
||||
name: string;
|
||||
snowpackPlugin: RendererSnowpackPlugin;
|
||||
client: string;
|
||||
server: string;
|
||||
}
|
||||
|
||||
const CONFIG_MODULE_BASE_NAME = '__astro_config.js';
|
||||
const CONFIG_MODULE_URL = `/_astro_frontend/${CONFIG_MODULE_BASE_NAME}`;
|
||||
|
||||
const DEFAULT_RENDERERS = [
|
||||
'@astrojs/renderer-vue',
|
||||
'@astrojs/renderer-svelte',
|
||||
'@astrojs/renderer-react',
|
||||
'@astrojs/renderer-preact'
|
||||
];
|
||||
|
||||
export class ConfigManager {
|
||||
private state: 'initial' | 'dirty' | 'clean' = 'initial';
|
||||
public snowpackRuntime: SnowpackServerRuntime | null = null;
|
||||
public configModuleId: string | null = null;
|
||||
private rendererNames!: string[];
|
||||
private version = 1;
|
||||
|
||||
constructor(
|
||||
private astroConfig: AstroConfig,
|
||||
private resolvePackageUrl: (pkgName: string) => Promise<string>,
|
||||
) {
|
||||
this.setRendererNames(this.astroConfig);
|
||||
}
|
||||
|
||||
markDirty() {
|
||||
this.state = 'dirty';
|
||||
}
|
||||
|
||||
async update() {
|
||||
if(this.needsUpdate() && this.snowpackRuntime) {
|
||||
// astro.config.mjs has changed, reload it.
|
||||
if(this.state === 'dirty') {
|
||||
const version = this.version++;
|
||||
const astroConfig = await loadConfig(this.astroConfig.projectRoot.pathname, `astro.config.mjs?version=${version}`);
|
||||
this.setRendererNames(astroConfig);
|
||||
}
|
||||
|
||||
await this.importModule(this.snowpackRuntime);
|
||||
this.state = 'clean';
|
||||
}
|
||||
}
|
||||
|
||||
isConfigModule(fileExt: string, filename: string) {
|
||||
return fileExt === '.js' && filename.endsWith(CONFIG_MODULE_BASE_NAME);
|
||||
}
|
||||
|
||||
isAstroConfig(filename: string) {
|
||||
const { projectRoot } = this.astroConfig;
|
||||
return new URL('./astro.config.mjs', projectRoot).pathname === filename;
|
||||
}
|
||||
|
||||
async buildRendererInstances(): Promise<RendererInstance[]> {
|
||||
const { projectRoot } = this.astroConfig;
|
||||
const rendererNames = this.rendererNames;
|
||||
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
|
||||
|
||||
const rendererInstances = (
|
||||
await Promise.all(
|
||||
rendererNames.map((rendererName) => {
|
||||
const entrypoint = pathToFileURL(resolveDependency(rendererName)).toString();
|
||||
return import(entrypoint);
|
||||
})
|
||||
)
|
||||
).map(({ default: raw }, i) => {
|
||||
const { name = rendererNames[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: RendererSnowpackPlugin;
|
||||
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),
|
||||
};
|
||||
});
|
||||
|
||||
return rendererInstances;
|
||||
}
|
||||
|
||||
async buildSource(contents: string): Promise<string> {
|
||||
const renderers = await this.buildRendererInstances();
|
||||
const rendererServerPackages = renderers.map(({ server }) => server);
|
||||
const rendererClientPackages = await Promise.all(renderers.map(({ client }) => this.resolvePackageUrl(client)));
|
||||
const result = /* js */ `${rendererServerPackages.map((pkg, i) => `import __renderer_${i} from "${pkg}";`).join('\n')}
|
||||
|
||||
import { setRenderers } from 'astro/dist/internal/__astro_component.js';
|
||||
|
||||
let rendererSources = [${rendererClientPackages.map(pkg => `"${pkg}"`).join(', ')}];
|
||||
let renderers = [${rendererServerPackages.map((_, i) => `__renderer_${i}`).join(', ')}];
|
||||
|
||||
${contents}
|
||||
`;
|
||||
return result;
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
return this.state === 'initial' || this.state === 'dirty';
|
||||
}
|
||||
|
||||
private setRendererNames(astroConfig: AstroConfig) {
|
||||
this.rendererNames = astroConfig.renderers || DEFAULT_RENDERERS;
|
||||
}
|
||||
|
||||
private async importModule(snowpackRuntime: SnowpackServerRuntime): Promise<void> {
|
||||
await snowpackRuntime!.importModule(CONFIG_MODULE_URL);
|
||||
}
|
||||
}
|
6
packages/astro/src/frontend/__astro_config.ts
Normal file
6
packages/astro/src/frontend/__astro_config.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
declare function setRenderers(sources: string[], renderers: any[]): void;
|
||||
|
||||
declare let rendererSources: string[];
|
||||
declare let renderers: any[];
|
||||
|
||||
setRenderers(rendererSources, renderers);
|
|
@ -1,3 +1,4 @@
|
|||
import type { Renderer } from '../@types/astro';
|
||||
import hash from 'shorthash';
|
||||
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
||||
import { generate } from 'astring';
|
||||
|
@ -7,22 +8,13 @@ import * as astro from './renderer-astro';
|
|||
// 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[];
|
||||
let rendererSources: string[] = [];
|
||||
let renderers: Renderer[] = [];
|
||||
|
||||
__rendererSources = ['', ...__rendererSources];
|
||||
__renderers = [astro, ...__renderers];
|
||||
export function setRenderers(_rendererSources: string[], _renderers: Renderer[]) {
|
||||
rendererSources = [''].concat(_rendererSources);
|
||||
renderers = [astro as Renderer].concat(_renderers);
|
||||
}
|
||||
|
||||
const rendererCache = new WeakMap();
|
||||
|
||||
|
@ -33,7 +25,7 @@ async function resolveRenderer(Component: any, props: any = {}, children?: strin
|
|||
}
|
||||
|
||||
const errors: Error[] = [];
|
||||
for (const __renderer of __renderers) {
|
||||
for (const __renderer of renderers) {
|
||||
// Yes, we do want to `await` inside of this loop!
|
||||
// __renderer.check can't be run in parallel, it
|
||||
// returns the first match and skips any subsequent checks
|
||||
|
@ -64,7 +56,7 @@ interface AstroComponentProps {
|
|||
|
||||
/** 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 rendererSource = rendererSources[renderers.findIndex((r) => r === renderer)];
|
||||
|
||||
const script = `<script type="module">
|
||||
import setup from '/_astro_frontend/hydrate/${hydrate}.js';
|
||||
|
@ -104,7 +96,7 @@ export const __astro_component = (Component: any, componentProps: AstroComponent
|
|||
if (!renderer) {
|
||||
// If the user only specifies a single renderer, but the check failed
|
||||
// for some reason... just default to their preferred renderer.
|
||||
renderer = __rendererSources.length === 2 ? __renderers[1] : null;
|
||||
renderer = rendererSources.length === 2 ? renderers[1] : null;
|
||||
|
||||
if (!renderer) {
|
||||
const name = getComponentName(Component, componentProps);
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { AstroConfig, CollectionResult, CollectionRSS, CreateCollection, Pa
|
|||
|
||||
import resolve from 'resolve';
|
||||
import { existsSync, promises as fs } from 'fs';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { posix as path } from 'path';
|
||||
import { performance } from 'perf_hooks';
|
||||
import {
|
||||
|
@ -22,6 +22,7 @@ import { debug, info } from './logger.js';
|
|||
import { configureSnowpackLogger } from './snowpack-logger.js';
|
||||
import { searchForPage } from './search.js';
|
||||
import snowpackExternals from './external.js';
|
||||
import { ConfigManager } from './config_manager.js';
|
||||
|
||||
interface RuntimeConfig {
|
||||
astroConfig: AstroConfig;
|
||||
|
@ -30,6 +31,7 @@ interface RuntimeConfig {
|
|||
snowpack: SnowpackDevServer;
|
||||
snowpackRuntime: SnowpackServerRuntime;
|
||||
snowpackConfig: SnowpackConfig;
|
||||
configManager: ConfigManager;
|
||||
}
|
||||
|
||||
// info needed for collection generation
|
||||
|
@ -54,7 +56,7 @@ configureSnowpackLogger(snowpackLogger);
|
|||
|
||||
/** Pass a URL to Astro to resolve and build */
|
||||
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
||||
const { logging, snowpackRuntime, snowpack } = config;
|
||||
const { logging, snowpackRuntime, snowpack, configManager } = config;
|
||||
const { buildOptions, devOptions } = config.astroConfig;
|
||||
|
||||
let origin = buildOptions.site ? new URL(buildOptions.site).origin : `http://localhost:${devOptions.port}`;
|
||||
|
@ -92,6 +94,9 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
let rss: { data: any[] & CollectionRSS } = {} as any;
|
||||
|
||||
try {
|
||||
if(configManager.needsUpdate()) {
|
||||
await configManager.update();
|
||||
}
|
||||
const mod = await snowpackRuntime.importModule(snowpackURL);
|
||||
debug(logging, 'resolve', `${reqPath} -> ${snowpackURL}`);
|
||||
|
||||
|
@ -306,31 +311,33 @@ interface RuntimeOptions {
|
|||
|
||||
interface CreateSnowpackOptions {
|
||||
mode: RuntimeMode;
|
||||
resolvePackageUrl?: (pkgName: string) => Promise<string>;
|
||||
resolvePackageUrl: (pkgName: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const DEFAULT_RENDERERS = ['@astrojs/renderer-vue', '@astrojs/renderer-svelte', '@astrojs/renderer-react', '@astrojs/renderer-preact'];
|
||||
|
||||
/** Create a new Snowpack instance to power Astro */
|
||||
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
|
||||
const { projectRoot, renderers = DEFAULT_RENDERERS } = astroConfig;
|
||||
const { projectRoot } = astroConfig;
|
||||
const { mode, resolvePackageUrl } = options;
|
||||
|
||||
const frontendPath = new URL('./frontend/', import.meta.url);
|
||||
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
|
||||
const isHmrEnabled = mode === 'development';
|
||||
|
||||
// The config manager takes care of the runtime config module (that handles setting renderers, mostly)
|
||||
const configManager = new ConfigManager(astroConfig, resolvePackageUrl);
|
||||
|
||||
let snowpack: SnowpackDevServer;
|
||||
let astroPluginOptions: {
|
||||
resolvePackageUrl?: (s: string) => Promise<string>;
|
||||
renderers?: { name: string; client: string; server: string }[];
|
||||
astroConfig: AstroConfig;
|
||||
hmrPort?: number;
|
||||
mode: RuntimeMode;
|
||||
configManager: ConfigManager;
|
||||
} = {
|
||||
astroConfig,
|
||||
resolvePackageUrl,
|
||||
mode,
|
||||
resolvePackageUrl,
|
||||
configManager,
|
||||
};
|
||||
|
||||
const mountOptions = {
|
||||
|
@ -344,46 +351,8 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
|||
(process.env as any).TAILWIND_DISABLE_TOUCH = true;
|
||||
}
|
||||
|
||||
const rendererInstances = (
|
||||
await Promise.all(
|
||||
renderers.map((renderer) => {
|
||||
const entrypoint = pathToFileURL(resolveDependency(renderer)).toString();
|
||||
return import(entrypoint);
|
||||
})
|
||||
)
|
||||
).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 rendererInstances = await configManager.buildRendererInstances();
|
||||
const knownEntrypoints = [].concat(...(rendererInstances.map((renderer) => [renderer.server, renderer.client]) as any));
|
||||
const rendererSnowpackPlugins = rendererInstances.filter((renderer) => renderer.snowpackPlugin).map((renderer) => renderer.snowpackPlugin) as string | [string, any];
|
||||
|
||||
|
@ -434,8 +403,9 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
|||
}
|
||||
);
|
||||
const snowpackRuntime = snowpack.getServerRuntime();
|
||||
astroPluginOptions.configManager.snowpackRuntime = snowpackRuntime;
|
||||
|
||||
return { snowpack, snowpackRuntime, snowpackConfig };
|
||||
return { snowpack, snowpackRuntime, snowpackConfig, configManager };
|
||||
}
|
||||
|
||||
/** Core Astro runtime */
|
||||
|
@ -449,6 +419,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
|
|||
snowpack: snowpackInstance,
|
||||
snowpackRuntime,
|
||||
snowpackConfig,
|
||||
configManager,
|
||||
} = await createSnowpack(astroConfig, {
|
||||
mode,
|
||||
resolvePackageUrl,
|
||||
|
@ -463,6 +434,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
|
|||
snowpack,
|
||||
snowpackRuntime,
|
||||
snowpackConfig,
|
||||
configManager,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in a new issue