diff options
author | 2021-06-21 08:44:45 -0400 | |
---|---|---|
committer | 2021-06-21 08:44:45 -0400 | |
commit | 491ff66603119928963fd58154a4a77246f342ca (patch) | |
tree | 2a47048bf51d3efe1fe2788e01db33766effc418 | |
parent | f04b82d47ea37a5e6bdfc8a125c9c42d170e6072 (diff) | |
download | astro-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.cjs | 61 | ||||
-rw-r--r-- | packages/astro/src/@types/astro.ts | 13 | ||||
-rw-r--r-- | packages/astro/src/config_manager.ts | 140 | ||||
-rw-r--r-- | packages/astro/src/frontend/__astro_config.ts | 6 | ||||
-rw-r--r-- | packages/astro/src/internal/__astro_component.ts | 30 | ||||
-rw-r--r-- | packages/astro/src/runtime.ts | 68 |
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 { |