astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
329 lines (328 loc) • 11.2 kB
JavaScript
import "./polyfill.js";
import { posix } from "node:path";
import { getDefaultClientDirectives } from "../core/client-directive/index.js";
import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schema.js";
import { validateConfig } from "../core/config/validate.js";
import { createKey } from "../core/encryption.js";
import { Logger } from "../core/logger/core.js";
import { nodeLogDestination } from "../core/logger/node.js";
import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
import { removeLeadingForwardSlash } from "../core/path.js";
import { RenderContext } from "../core/render-context.js";
import { getParts, validateSegment } from "../core/routing/manifest/create.js";
import { getPattern } from "../core/routing/manifest/pattern.js";
import { ContainerPipeline } from "./pipeline.js";
function createManifest(manifest, renderers, middleware) {
function middlewareInstance() {
return {
onRequest: middleware ?? NOOP_MIDDLEWARE_FN
};
}
return {
hrefRoot: import.meta.url,
trailingSlash: manifest?.trailingSlash ?? ASTRO_CONFIG_DEFAULTS.trailingSlash,
buildFormat: manifest?.buildFormat ?? ASTRO_CONFIG_DEFAULTS.build.format,
compressHTML: manifest?.compressHTML ?? ASTRO_CONFIG_DEFAULTS.compressHTML,
assets: manifest?.assets ?? /* @__PURE__ */ new Set(),
assetsPrefix: manifest?.assetsPrefix ?? void 0,
entryModules: manifest?.entryModules ?? {},
routes: manifest?.routes ?? [],
adapterName: "",
clientDirectives: manifest?.clientDirectives ?? getDefaultClientDirectives(),
renderers: renderers ?? manifest?.renderers ?? [],
base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base,
componentMetadata: manifest?.componentMetadata ?? /* @__PURE__ */ new Map(),
inlinedScripts: manifest?.inlinedScripts ?? /* @__PURE__ */ new Map(),
i18n: manifest?.i18n,
checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance,
key: createKey()
};
}
class experimental_AstroContainer {
#pipeline;
/**
* Internally used to check if the container was created with a manifest.
* @private
*/
#withManifest = false;
/**
* Internal function responsible for importing a renderer
* @private
*/
#getRenderer;
constructor({
streaming = false,
manifest,
renderers,
resolve,
astroConfig
}) {
this.#pipeline = ContainerPipeline.create({
logger: new Logger({
level: "info",
dest: nodeLogDestination
}),
manifest: createManifest(manifest, renderers),
streaming,
serverLike: true,
renderers: renderers ?? manifest?.renderers ?? [],
resolve: async (specifier) => {
if (this.#withManifest) {
return this.#containerResolve(specifier, astroConfig);
} else if (resolve) {
return resolve(specifier);
}
return specifier;
}
});
}
async #containerResolve(specifier, astroConfig) {
const found = this.#pipeline.manifest.entryModules[specifier];
if (found) {
return new URL(found, astroConfig?.build.client).toString();
}
return found;
}
/**
* Creates a new instance of a container.
*
* @param {AstroContainerOptions=} containerOptions
*/
static async create(containerOptions = {}) {
const { streaming = false, manifest, renderers = [], resolve } = containerOptions;
const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container");
return new experimental_AstroContainer({
streaming,
manifest,
renderers,
astroConfig,
resolve
});
}
/**
* Use this function to manually add a **server** renderer to the container.
*
* This function is preferred when you require to use the container with a renderer in environments such as on-demand pages.
*
* ## Example
*
* ```js
* import reactRenderer from "@astrojs/react/server.js";
* import vueRenderer from "@astrojs/vue/server.js";
* import customRenderer from "../renderer/customRenderer.js";
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
*
* const container = await AstroContainer.create();
* container.addServerRenderer(reactRenderer);
* container.addServerRenderer(vueRenderer);
* container.addServerRenderer("customRenderer", customRenderer);
* ```
*
* @param options {object}
* @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
* @param options.renderer The server renderer exported by integration.
*/
addServerRenderer(options) {
const { renderer, name } = options;
if (!renderer.check || !renderer.renderToStaticMarkup) {
throw new Error(
"The renderer you passed isn't valid. A renderer is usually an object that exposes the `check` and `renderToStaticMarkup` functions.\nUsually, the renderer is exported by a /server.js entrypoint e.g. `import renderer from '@astrojs/react/server.js'`"
);
}
if (isNamedRenderer(renderer)) {
this.#pipeline.manifest.renderers.push({
name: renderer.name,
ssr: renderer
});
} else {
this.#pipeline.manifest.renderers.push({
name,
ssr: renderer
});
}
}
/**
* Use this function to manually add a **client** renderer to the container.
*
* When rendering components that use the `client:*` directives, you need to use this function.
*
* ## Example
*
* ```js
* import reactRenderer from "@astrojs/react/server.js";
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
*
* const container = await AstroContainer.create();
* container.addServerRenderer(reactRenderer);
* container.addClientRenderer({
* name: "@astrojs/react",
* entrypoint: "@astrojs/react/client.js"
* });
* ```
*
* @param options {object}
* @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
* @param options.entrypoint The entrypoint of the client renderer.
*/
addClientRenderer(options) {
const { entrypoint, name } = options;
const rendererIndex = this.#pipeline.manifest.renderers.findIndex((r) => r.name === name);
if (rendererIndex === -1) {
throw new Error(
"You tried to add the " + name + " client renderer, but its server renderer wasn't added. You must add the server renderer first. Use the `addServerRenderer` function."
);
}
const renderer = this.#pipeline.manifest.renderers[rendererIndex];
renderer.clientEntrypoint = entrypoint;
this.#pipeline.manifest.renderers[rendererIndex] = renderer;
}
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
// @ematipico: I plan to use it for a possible integration that could help people
static async createFromManifest(manifest) {
const astroConfig = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container");
const container = new experimental_AstroContainer({
manifest,
astroConfig
});
container.#withManifest = true;
return container;
}
#insertRoute({
path,
componentInstance,
params = {},
type = "page"
}) {
const pathUrl = new URL(path, "https://example.com");
const routeData = this.#createRoute(pathUrl, params, type);
this.#pipeline.manifest.routes.push({
routeData,
file: "",
links: [],
styles: [],
scripts: []
});
this.#pipeline.insertRoute(routeData, componentInstance);
return routeData;
}
/**
* @description
* It renders a component and returns the result as a string.
*
* ## Example
*
* ```js
* import Card from "../src/components/Card.astro";
*
* const container = await AstroContainer.create();
* const result = await container.renderToString(Card);
*
* console.log(result); // it's a string
* ```
*
*
* @param {AstroComponentFactory} component The instance of the component.
* @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
*/
async renderToString(component, options = {}) {
const response = await this.renderToResponse(component, options);
return await response.text();
}
/**
* @description
* It renders a component and returns the `Response` as result of the rendering phase.
*
* ## Example
*
* ```js
* import Card from "../src/components/Card.astro";
*
* const container = await AstroContainer.create();
* const response = await container.renderToResponse(Card);
*
* console.log(response.status); // it's a number
* ```
*
*
* @param {AstroComponentFactory} component The instance of the component.
* @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
*/
async renderToResponse(component, options = {}) {
const { routeType = "page", slots } = options;
const request = options?.request ?? new Request("https://example.com/");
const url = new URL(request.url);
const componentInstance = routeType === "endpoint" ? component : this.#wrapComponent(component, options.params);
const routeData = this.#insertRoute({
path: request.url,
componentInstance,
params: options.params,
type: routeType
});
const renderContext = await RenderContext.create({
pipeline: this.#pipeline,
routeData,
status: 200,
request,
pathname: url.pathname,
locals: options?.locals ?? {},
partial: options?.partial ?? true,
clientAddress: ""
});
if (options.params) {
renderContext.params = options.params;
}
if (options.props) {
renderContext.props = options.props;
}
return renderContext.render(componentInstance, slots);
}
#createRoute(url, params, type) {
const segments = removeLeadingForwardSlash(url.pathname).split(posix.sep).filter(Boolean).map((s) => {
validateSegment(s);
return getParts(s, url.pathname);
});
return {
route: url.pathname,
component: "",
generate(_data) {
return "";
},
params: Object.keys(params),
pattern: getPattern(
segments,
ASTRO_CONFIG_DEFAULTS.base,
ASTRO_CONFIG_DEFAULTS.trailingSlash
),
prerender: false,
segments,
type,
fallbackRoutes: [],
isIndex: false,
origin: "internal"
};
}
/**
* If the provided component isn't a default export, the function wraps it in an object `{default: Component }` to mimic the default export.
* @param componentFactory
* @param params
* @private
*/
#wrapComponent(componentFactory, params) {
if (params) {
return {
default: componentFactory,
getStaticPaths() {
return [{ params }];
}
};
}
return { default: componentFactory };
}
}
function isNamedRenderer(renderer) {
return !!renderer?.name;
}
export {
experimental_AstroContainer
};