summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-06-21 08:44:45 -0400
committerGravatar GitHub <noreply@github.com> 2021-06-21 08:44:45 -0400
commit491ff66603119928963fd58154a4a77246f342ca (patch)
tree2a47048bf51d3efe1fe2788e01db33766effc418
parentf04b82d47ea37a5e6bdfc8a125c9c42d170e6072 (diff)
downloadastro-491ff66603119928963fd58154a4a77246f342ca.tar.gz
astro-491ff66603119928963fd58154a4a77246f342ca.tar.zst
astro-491ff66603119928963fd58154a4a77246f342ca.zip
Allow renderers configuration to update (#489)
* Start of dynamic renderers * Implementation
-rw-r--r--packages/astro/snowpack-plugin.cjs61
-rw-r--r--packages/astro/src/@types/astro.ts13
-rw-r--r--packages/astro/src/config_manager.ts140
-rw-r--r--packages/astro/src/frontend/__astro_config.ts6
-rw-r--r--packages/astro/src/internal/__astro_component.ts30
-rw-r--r--packages/astro/src/runtime.ts68
6 files changed, 219 insertions, 99 deletions
diff --git a/packages/astro/snowpack-plugin.cjs b/packages/astro/snowpack-plugin.cjs
index 47d784975..a50816089 100644
--- a/packages/astro/snowpack-plugin.cjs
+++ b/packages/astro/snowpack-plugin.cjs
@@ -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 = {
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index db0b77589..f76f0c96f 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -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;
+ }>;
+} \ No newline at end of file
diff --git a/packages/astro/src/config_manager.ts b/packages/astro/src/config_manager.ts
new file mode 100644
index 000000000..808ed4246
--- /dev/null
+++ b/packages/astro/src/config_manager.ts
@@ -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);
+ }
+} \ No newline at end of file
diff --git a/packages/astro/src/frontend/__astro_config.ts b/packages/astro/src/frontend/__astro_config.ts
new file mode 100644
index 000000000..1765ffffc
--- /dev/null
+++ b/packages/astro/src/frontend/__astro_config.ts
@@ -0,0 +1,6 @@
+declare function setRenderers(sources: string[], renderers: any[]): void;
+
+declare let rendererSources: string[];
+declare let renderers: any[];
+
+setRenderers(rendererSources, renderers); \ No newline at end of file
diff --git a/packages/astro/src/internal/__astro_component.ts b/packages/astro/src/internal/__astro_component.ts
index 4976fe84f..1e0a75c16 100644
--- a/packages/astro/src/internal/__astro_component.ts
+++ b/packages/astro/src/internal/__astro_component.ts
@@ -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[];
-
-__rendererSources = ['', ...__rendererSources];
-__renderers = [astro, ...__renderers];
+let rendererSources: string[] = [];
+let renderers: Renderer[] = [];
+
+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);
diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts
index a7bd55fed..58606d3a0 100644
--- a/packages/astro/src/runtime.ts
+++ b/packages/astro/src/runtime.ts
@@ -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 {