Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Astro Integration System #2820

Merged
merged 9 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"compiled/*",
"packages/markdown/*",
"packages/renderers/*",
"packages/integrations/*",
"packages/*",
"examples/*",
"examples/component/demo",
Expand Down
6 changes: 1 addition & 5 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,14 @@
"dev": "astro-scripts dev \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "mocha --parallel --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
"test:match": "mocha --timeout 20000 -g"
},
"dependencies": {
"@astrojs/compiler": "^0.12.1",
"@astrojs/language-server": "^0.8.10",
"@astrojs/markdown-remark": "^0.6.4",
"@astrojs/prism": "0.4.0",
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "0.5.0",
"@astrojs/renderer-svelte": "0.5.2",
"@astrojs/renderer-vue": "0.4.0",
"@astrojs/webapi": "^0.11.0",
"@babel/core": "^7.17.7",
"@babel/traverse": "^7.17.3",
Expand Down
107 changes: 75 additions & 32 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { AddressInfo } from 'net';
import type * as babel from '@babel/core';
import type * as vite from 'vite';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request';
import type * as vite from 'vite';

export interface AstroBuiltinProps {
'client:load'?: boolean;
Expand Down Expand Up @@ -127,21 +128,30 @@ export interface AstroUserConfig {

/**
* @docs
* @name renderers
* @type {string[]}
* @default `['@astrojs/renderer-svelte','@astrojs/renderer-vue','@astrojs/renderer-react','@astrojs/renderer-preact']`
* @name integrations
* @type {AstroIntegration[]}
* @default `["react", "preact", "svelte", "vue"]`
FredKSchott marked this conversation as resolved.
Show resolved Hide resolved
* @description
* Set the UI framework renderers for your project. Framework renderers are what power Astro's ability to use other frameworks inside of your project, like React, Svelte, and Vue.
* Add Integrations to your project to extend Astro.
*
* Integrations are your one-stop shop to add new frameworks (like Solid.js), new features (like sitemaps), and new libraries (like Partytown and Turbolinks).
*
* Setting this configuration will disable Astro's default framework support, so you will need to provide a renderer for every framework that you want to use.
* Setting this configuration will disable Astro's default integration, so it is recommended to provide a renderer for every framework that you use:
*
* Note: Integrations are currently under active development, and only first-party integrations are supported. In the future, 3rd-party integrations will be allowed.
*
* ```js
* import react from '@astrojs/react';
* import vue from '@astrojs/vue';
* {
* // Use Astro + React, with no other frameworks.
* renderers: ['@astrojs/renderer-react']
* // Example: Use Astro with Vue + React, and no other frameworks.
* integrations: [react(), vue()]
* }
* ```
*/
integrations?: AstroIntegration[];

/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
renderers?: string[];
FredKSchott marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down Expand Up @@ -170,6 +180,7 @@ export interface AstroUserConfig {
* }
* ```
*/
/** Options for rendering markdown content */
markdownOptions?: {
render?: MarkdownRenderOptions;
};
Expand Down Expand Up @@ -379,7 +390,7 @@ export interface AstroUserConfig {

/**
* @docs
* @name devOptions.vite
* @name vite
* @type {vite.UserConfig}
* @description
*
Expand Down Expand Up @@ -421,11 +432,33 @@ export interface AstroUserConfig {
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// }

/**
* IDs for different stages of JS script injection:
* - "before-hydration": Imported client-side, before the hydration script runs. Processed & resolved by Vite.
* - "head-inline": Injected into a script tag in the `<head>` of every page. Not processed or resolved by Vite.
* - "page": Injected into the JavaScript bundle of every page. Processed & resolved by Vite.
* - "page-ssr": Injected into the frontmatter of every Astro page. Processed & resolved by Vite.
*/
type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';

/**
* Resolved Astro Config
* Config with user settings along with all defaults filled in.
*/
export type AstroConfig = z.output<typeof AstroConfigSchema>;
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// Public:
// This is a more detailed type than zod validation gives us.
// TypeScript still confirms zod validation matches this type.
integrations: AstroIntegration[];
// Private:
// We have a need to pass context based on configured state,
// that is different from the user-exposed configuration.
// TODO: Create an AstroConfig class to manage this, long-term.
_ctx: {
Copy link
Contributor

@matthewp matthewp Mar 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I will die on this hill (I might) but I think mixing configuration and state is a mistake that will be hard to unravel. Probably the right thing to do is create a class/object that wraps the config and then pass that everywhere (I believe that is what ResolvedConfig is for in Vite).

But that would involve updating a crazy amount of code, so I understand not wanting to do that.

How about a compromise where this context object is kept in a global WeakMap?

interface Context { ... }

const contexts = new WeakMap<AstroConfig, Context>();
const getContext = (config: AstroConfig) => contexts.get(config);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this. Not sure the best solution but I don't love shoving a stateful object in here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I tried to leave the TODO to capture that need here. I agree, a context object isn't really a property of your config. If anything, it's the opposite, so maybe AstroContext becomes the class that manages your config and other stuff.

If we're talking whats in scope for this PR and the short-term, I think I prefer the _ctx hack vs. maintaining a weakmap separately. It's not a hill that I'll die on either, but I don't see that as any easier/harder to unravel than _ctx.

The compromise that I'd suggest instead would be to fast-track that larger Class/Object refactor instead of letting this sit for to long post-merge. If we could commit to that (maybe @bholmesdev could take a look next week?) then I think _ctx is the better short-term path.

renderers: AstroRenderer[];
scripts: { stage: InjectedScriptStage; content: string }[];
};
}

export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;

Expand Down Expand Up @@ -560,38 +593,48 @@ export interface EndpointHandler {
[method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response;
}

/**
* Astro Renderer
* Docs: https://docs.astro.build/reference/renderer-reference/
*/
export interface Renderer {
/** Name of the renderer (required) */
export interface AstroRenderer {
/** Name of the renderer. */
name: string;
/** Import statement for renderer */
source?: string;
/** Import statement for the server renderer */
serverEntry: string;
/** Scripts to be injected before component */
polyfills?: string[];
/** Polyfills that need to run before hydration ever occurs */
hydrationPolyfills?: string[];
/** Import entrypoint for the client/browser renderer. */
clientEntrypoint?: string;
/** Import entrypoint for the server/build/ssr renderer. */
serverEntrypoint: string;
/** JSX identifier (e.g. 'react' or 'solid-js') */
jsxImportSource?: string;
/** Babel transform options */
jsxTransformOptions?: JSXTransformFn;
/** Utilies for server-side rendering */
}

export interface SSRLoadedRenderer extends AstroRenderer {
ssr: {
check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{
html: string;
}>;
};
/** Return configuration object for Vite ("options" should match https://vitejs.dev/guide/api-plugin.html#config) */
viteConfig?: (options: { mode: 'string'; command: 'build' | 'serve' }) => Promise<vite.InlineConfig>;
/** @deprecated Don’t try and build these dependencies for client (deprecated in 0.21) */
external?: string[];
/** @deprecated Clientside requirements (deprecated in 0.21) */
knownEntrypoints?: string[];
}

export interface AstroIntegration {
/** The name of the integration. */
name: string;
/** The different hooks available to extend. */
hooks: {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void;
}) => void;
'astro:config:done'?: (options: { config: AstroConfig }) => void | Promise<void>;
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>;
'astro:build:start'?: () => void | Promise<void>;
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL }) => void | Promise<void>;
};
}

export type RouteType = 'page' | 'endpoint';
Expand Down Expand Up @@ -665,7 +708,7 @@ export interface SSRElement {
}

export interface SSRMetadata {
renderers: Renderer[];
renderers: SSRLoadedRenderer[];
pathname: string;
legacyBuild: boolean;
}
Expand Down
20 changes: 6 additions & 14 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { ComponentInstance, ManifestData, RouteData, Renderer } from '../../@types/astro';
import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } from '../../@types/astro';
import type { SSRManifest as Manifest, RouteInfo } from './types';

import { defaultLogOptions } from '../logger.js';
import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { createRenderer } from '../render/renderer.js';
import { prependForwardSlash } from '../path.js';

export class App {
Expand All @@ -15,7 +14,7 @@ export class App {
#rootFolder: URL;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#routeCache: RouteCache;
#renderersPromise: Promise<Renderer[]>;
#renderersPromise: Promise<SSRLoadedRenderer[]>;

constructor(manifest: Manifest, rootFolder: URL) {
this.#manifest = manifest;
Expand Down Expand Up @@ -84,18 +83,11 @@ export class App {
status: 200,
});
}
async #loadRenderers(): Promise<Renderer[]> {
const rendererNames = this.#manifest.renderers;
async #loadRenderers(): Promise<SSRLoadedRenderer[]> {
return await Promise.all(
rendererNames.map(async (rendererName) => {
return createRenderer(rendererName, {
renderer(name) {
return import(name);
},
server(entry) {
return import(entry);
},
});
this.#manifest.renderers.map(async (renderer) => {
const mod = (await import(renderer.serverEntrypoint)) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default };
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RouteData, SerializedRouteData, MarkdownRenderOptions } from '../../@types/astro';
import type { RouteData, SerializedRouteData, MarkdownRenderOptions, AstroRenderer } from '../../@types/astro';

export interface RouteInfo {
routeData: RouteData;
Expand All @@ -17,7 +17,7 @@ export interface SSRManifest {
markdown: {
render: MarkdownRenderOptions;
};
renderers: string[];
renderers: AstroRenderer[];
entryModules: Record<string, string>;
}

Expand Down
21 changes: 12 additions & 9 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { collectPagesData } from './page-data.js';
import { build as scanBasedBuild } from './scan-based-build.js';
import { staticBuild } from './static-build.js';
import { RouteCache } from '../render/route-cache.js';
import { runHookBuildDone, runHookBuildStart, runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js';

export interface BuildOptions {
mode?: string;
Expand Down Expand Up @@ -57,23 +58,23 @@ class AstroBuilder {
const timer: Record<string, number> = {};
timer.init = performance.now();
timer.viteStart = performance.now();
this.config = await runHookConfigSetup({ config: this.config, command: 'build' });
const viteConfig = await createVite(
vite.mergeConfig(
{
mode: this.mode,
server: {
hmr: false,
middlewareMode: 'ssr',
},
{
mode: this.mode,
server: {
hmr: false,
middlewareMode: 'ssr',
},
this.config.vite || {}
),
},
{ astroConfig: this.config, logging, mode: 'build' }
);
await runHookConfigDone({ config: this.config });
this.viteConfig = viteConfig;
const viteServer = await vite.createServer(viteConfig);
this.viteServer = viteServer;
debug('build', timerMessage('Vite started', timer.viteStart));
await runHookBuildStart({ config: this.config });

timer.loadStart = performance.now();
const { assets, allPages } = await collectPagesData({
Expand Down Expand Up @@ -160,6 +161,8 @@ class AstroBuilder {

// You're done! Time to clean up.
await viteServer.close();
await runHookBuildDone({ config: this.config, pages: pageNames });

if (logging.level && levels[logging.level] <= levels['info']) {
await this.printStats({ logging, timeStart: timer.init, pageCount: pageNames.length });
}
Expand Down
Loading